Performance improvement for joins where outer side is unique

Started by David Rowleyabout 11 years ago131 messages
#1David Rowley
dgrowleyml@gmail.com
2 attachment(s)

Hi,

I've been hacking a bit at the join code again... This time I've been
putting some effort into optimising the case where the inner side of the
join is known to be unique.
For example, given the tables:

create table t1 (id int primary key);
create table t2 (id int primary key);

And query such as:

select * from t1 left outer join t2 on t1.id=t2.id;

It is possible to deduce at planning time that "for each row in the outer
relation, only 0 or 1 rows can exist in the inner relation", (inner being
t2)

In all of our join algorithms in the executor, if the join type is SEMI,
we skip to the next outer row once we find a matching inner row. This is
because we don't want to allow duplicate rows in the inner side to
duplicate outer rows in the result set. Obviously this is required per SQL
spec. I believe we can also skip to the next outer row in this case when
we've managed to prove that no other row can possibly exist that matches
the current outer row, due to a unique index or group by/distinct clause
(for subqueries).

I've attached a patch which aims to prove this idea works. Although so far
I've only gotten around to implementing this for outer joins.

Since code already existed in analyzejoin.c which aimed to determine if a
join to a relation was unique on the join's condition, the patch is pretty
much just some extra plumbing and a small rewire of analyzejoin.c, which
just aims to get the "unique_inner" bool value down to the join node.

The performance improvement is somewhere around 5-10% for hash joins, and a
bit less for merge join. In theory it could be 50% for nested loop joins.
In my life in the OLTP world, these "unique joins" pretty much cover all
joins that ever happen ever. Perhaps the OLAP world is different, so from
my point of view this is going to be a very nice performance gain.

I'm seeing this patch (or a more complete version) as the first step to a
whole number of other planner improvements:

A couple of examples of those are:

1.
explain select * from sales s inner join product p on p.productid =
s.productid order by s.productid,p.name;

The current plan for this looks like:
QUERY PLAN
--------------------------------------------------------------------------------
Sort (cost=149.80..152.80 rows=1200 width=46)
Sort Key: s.productid, p.name
-> Hash Join (cost=37.00..88.42 rows=1200 width=46)
Hash Cond: (s.productid = p.productid)
-> Seq Scan on sales s (cost=0.00..31.40 rows=2140 width=8)
-> Hash (cost=22.00..22.00 rows=1200 width=38)
-> Seq Scan on product p (cost=0.00..22.00 rows=1200
width=38)

But in reality we could have executed this with a simple merge join using
the PK of product (productid) to provide presorted input. The extra sort
column on p.name is redundant due to productid being unique.

The UNION planning is crying out for help for cases like this. Where it
could provide sorted input on a unique index, providing the union's
targetlist contained all of the unique index's columns, and we also managed
to find an index on the other part of the union on the same set of columns,
then we could perform a Merge Append and a Unique. This would cause a
signification improvement in execution time for these types of queries, as
the current planner does an append/sort/unique, which especially sucks for
plans with a LIMIT clause.

I think this should solve some of the problems that Kyotarosan encountered
in his episode of dancing with indices over here ->
/messages/by-id/20131031.194310.212107585.horiguchi.kyotaro@lab.ntt.co.jp
where he was unable to prove that he could trim down sort nodes once all of
the columns of a unique index had been seen in the order by due to not
knowing if joins were going to duplicate the outer rows.

2.
It should also be possible to reuse a join in situations like:
create view product_sales as select p.productid,sum(s.qty) soldqty from
product p inner join sales s group by p.productid;

Where the consumer does:

select ps.*,p.current_price from product_sales ps inner join product p on
ps.productid = p.productid;

Here we'd currently join the product table twice, both times on the same
condition, but we could safely not bother doing that if we knew that the
join could never match more than 1 row on the inner side. Unfortunately I
deal with horrid situations like this daily, where people have used views
from within views, and all the same tables end up getting joined multiple
times :-(

Of course, both 1 and 2 should be considered separately from the attached
patch, this was just to show where it might lead.

Performance of the patch are as follows:

Test case:
create table t1 (id int primary key);
create table t2 (id int primary key);

insert into t1 select x.x from generate_series(1,1000000) x(x);
insert into t2 select x.x from generate_series(1,1000000) x(x);

vacuum analyze;

Query:
select count(t2.id) from t1 left outer join t2 on t1.id=t2.id;

Values are in transactions per second.

JOIN type Patched Unpatched new runtime
hash 1.295764 1.226188 94.63%
merge 1.786248 1.776605 99.46%
nestloop 0.465356 0.443015 95.20%

Things improve a bit more when using a varchar instead of an int:

hash 1.198821 1.102183 91.94%
I've attached the full benchmark results so as not to spam the thread.

Comments are welcome

Regards

David Rowley

Attachments:

unijoin_benchmark.txttext/plain; charset=US-ASCII; name=unijoin_benchmark.txtDownload
unijoins_v1.patchapplication/octet-stream; name=unijoins_v1.patchDownload
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 7eec3f3..5350ab5 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semijoin or if the planner found that no more than
+					 * 1 inner row will match a single outer row, we'll
+					 * consider returning the first match, but after that we're
+					 * done with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI || node->js.unique_inner)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -469,6 +471,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 		ExecInitExpr((Expr *) node->join.plan.qual,
 					 (PlanState *) hjstate);
 	hjstate->js.jointype = node->join.jointype;
+	hjstate->js.unique_inner = node->join.unique_inner;
 	hjstate->js.joinqual = (List *)
 		ExecInitExpr((Expr *) node->join.joinqual,
 					 (PlanState *) hjstate);
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index fdf2f4c..6122b47 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -832,10 +832,12 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semijoin or if the planner found that no more than
+					 * 1 inner row will match a single outer row, we'll
+					 * consider returning the first match, but after that we're
+					 * done with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI || node->js.unique_inner )
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1503,6 +1505,7 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 		ExecInitExpr((Expr *) node->join.plan.qual,
 					 (PlanState *) mergestate);
 	mergestate->js.jointype = node->join.jointype;
+	mergestate->js.unique_inner = node->join.unique_inner;
 	mergestate->js.joinqual = (List *)
 		ExecInitExpr((Expr *) node->join.joinqual,
 					 (PlanState *) mergestate);
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 6cdd4ff..a89be06 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,11 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * In a semijoin or if the planner found that no more than 1 inner
+			 * row will match a single outer row, we'll consider returning the
+			 * first match, but after that we're done with this outer tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.jointype == JOIN_SEMI || node->js.unique_inner)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -327,6 +328,7 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 		ExecInitExpr((Expr *) node->join.plan.qual,
 					 (PlanState *) nlstate);
 	nlstate->js.jointype = node->join.jointype;
+	nlstate->js.unique_inner = node->join.unique_inner;
 	nlstate->js.joinqual = (List *)
 		ExecInitExpr((Expr *) node->join.joinqual,
 					 (PlanState *) nlstate);
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index a737d7d..1ea346a 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -639,6 +639,7 @@ CopyJoinFields(const Join *from, Join *newnode)
 	CopyPlanFields((const Plan *) from, (Plan *) newnode);
 
 	COPY_SCALAR_FIELD(jointype);
+	COPY_SCALAR_FIELD(unique_inner);
 	COPY_NODE_FIELD(joinqual);
 }
 
@@ -1938,6 +1939,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(unique_inner);
 	COPY_NODE_FIELD(join_quals);
 
 	return newnode;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 14e190a..f6672c6 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -796,6 +796,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(unique_inner);
 	COMPARE_NODE_FIELD(join_quals);
 
 	return true;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index e99d416..e764bda 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -32,12 +32,33 @@
 #include "utils/lsyscache.h"
 
 /* local functions */
+static bool join_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo);
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/* it may have already been marked by a previous call. */
+		if (sjinfo->unique_inner)
+			continue;
+
+		/*
+		 * Determine if the inner side of the join can produce at most 1 row
+		 * for any single outer row.
+		 */
+		sjinfo->unique_inner = join_is_unique_join(root, sjinfo);
+	}
+}
 
 /*
  * remove_useless_joins
@@ -91,6 +112,16 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * The removal of the join could have caused push down quals to
+		 * have been removed, which could previously have stopped us
+		 * from marking the join as a unique join. We'd better make
+		 * another pass over each join to make sure otherwise we may fail
+		 * to remove other joins which had a join condition on the join
+		 * that we just removed.
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -135,36 +166,21 @@ clause_sides_match_join(RestrictInfo *rinfo, Relids outerrelids,
 	return false;				/* no good for these input relations */
 }
 
-/*
- * join_is_removable
- *	  Check whether we need not perform this special join at all, because
- *	  it will just duplicate its left input.
- *
- * This is true for a left join for which the join condition cannot match
- * more than one inner-side row.  (There are other possibly interesting
- * cases, but we don't have the infrastructure to prove them.)  We also
- * have to check that the inner side doesn't generate any variables needed
- * above the join.
- */
 static bool
-join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+join_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
 	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
 	ListCell   *l;
-	int			attroff;
+	List	   *clause_list = NIL;
 
-	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
-	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
-		sjinfo->delay_upper_joins)
+	/* if there's any delayed upper joins then we'll need to punt. */
+	if (sjinfo->delay_upper_joins)
 		return false;
 
+	/* if there's more than 1 relation involved then punt */
 	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
 		return false;
 
@@ -174,9 +190,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		return false;
 
 	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
 	 */
 	if (innerrel->rtekind == RTE_RELATION)
 	{
@@ -207,53 +223,6 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
 
 	/*
-	 * We can't remove the join if any inner-rel attributes are used above the
-	 * join.
-	 *
-	 * Note that this test only detects use of inner-rel attributes in higher
-	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
-	 *
-	 * As a micro-optimization, it seems better to start with max_attr and
-	 * count down rather than starting with min_attr and counting up, on the
-	 * theory that the system attributes are somewhat less likely to be wanted
-	 * and should be tested last.
-	 */
-	for (attroff = innerrel->max_attr - innerrel->min_attr;
-		 attroff >= 0;
-		 attroff--)
-	{
-		if (!bms_is_subset(innerrel->attr_needed[attroff], joinrelids))
-			return false;
-	}
-
-	/*
-	 * Similarly check that the inner rel isn't needed by any PlaceHolderVars
-	 * that will be used above the join.  We only need to fail if such a PHV
-	 * actually references some inner-rel attributes; but the correct check
-	 * for that is relatively expensive, so we first check against ph_eval_at,
-	 * which must mention the inner rel if the PHV uses any inner-rel attrs as
-	 * non-lateral references.  Note that if the PHV's syntactic scope is just
-	 * the inner rel, we can't drop the rel even if the PHV is variable-free.
-	 */
-	foreach(l, root->placeholder_list)
-	{
-		PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(l);
-
-		if (bms_is_subset(phinfo->ph_needed, joinrelids))
-			continue;			/* PHV is not used above the join */
-		if (bms_overlap(phinfo->ph_lateral, innerrel->relids))
-			return false;		/* it references innerrel laterally */
-		if (!bms_overlap(phinfo->ph_eval_at, innerrel->relids))
-			continue;			/* it definitely doesn't reference innerrel */
-		if (bms_is_subset(phinfo->ph_eval_at, innerrel->relids))
-			return false;		/* there isn't any other place to eval PHV */
-		if (bms_overlap(pull_varnos((Node *) phinfo->ph_var->phexpr),
-						innerrel->relids))
-			return false;		/* it does reference innerrel */
-	}
-
-	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
 	 * mergejoinable then it behaves like equality for some btree opclass, so
@@ -368,6 +337,94 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	return false;
 }
 
+/*
+ * join_is_removable
+ *	  Check whether we need not perform this special join at all, because
+ *	  it will just duplicate its left input.
+ *
+ * This is true for a left join for which the join condition cannot match
+ * more than one inner-side row.  (There are other possibly interesting
+ * cases, but we don't have the infrastructure to prove them.)  We also
+ * have to check that the inner side doesn't generate any variables needed
+ * above the join.
+ */
+static bool
+join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	int			attroff;
+	ListCell   *l;
+
+	/*
+	 * We can only remove LEFT JOINs where the inner side is unique on the
+	 * join condition.
+	 */
+	if (sjinfo->jointype != JOIN_LEFT ||
+		sjinfo->unique_inner == false)
+		return false;
+
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
+	/*
+	 * We can't remove the join if any inner-rel attributes are used above the
+	 * join.
+	 *
+	 * Note that this test only detects use of inner-rel attributes in higher
+	 * join conditions and the target list.  There might be such attributes in
+	 * pushed-down conditions at this join, too.  We check that case below.
+	 *
+	 * As a micro-optimization, it seems better to start with max_attr and
+	 * count down rather than starting with min_attr and counting up, on the
+	 * theory that the system attributes are somewhat less likely to be wanted
+	 * and should be tested last.
+	 */
+	for (attroff = innerrel->max_attr - innerrel->min_attr;
+		 attroff >= 0;
+		 attroff--)
+	{
+		if (!bms_is_subset(innerrel->attr_needed[attroff], joinrelids))
+			return false;
+	}
+
+	/*
+	 * Similarly check that the inner rel isn't needed by any PlaceHolderVars
+	 * that will be used above the join.  We only need to fail if such a PHV
+	 * actually references some inner-rel attributes; but the correct check
+	 * for that is relatively expensive, so we first check against ph_eval_at,
+	 * which must mention the inner rel if the PHV uses any inner-rel attrs as
+	 * non-lateral references.  Note that if the PHV's syntactic scope is just
+	 * the inner rel, we can't drop the rel even if the PHV is variable-free.
+	 */
+	foreach(l, root->placeholder_list)
+	{
+		PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(l);
+
+		if (bms_is_subset(phinfo->ph_needed, joinrelids))
+			continue;			/* PHV is not used above the join */
+		if (bms_overlap(phinfo->ph_lateral, innerrel->relids))
+			return false;		/* it references innerrel laterally */
+		if (!bms_overlap(phinfo->ph_eval_at, innerrel->relids))
+			continue;			/* it definitely doesn't reference innerrel */
+		if (bms_is_subset(phinfo->ph_eval_at, innerrel->relids))
+			return false;		/* there isn't any other place to eval PHV */
+		if (bms_overlap(pull_varnos((Node *) phinfo->ph_var->phexpr),
+						innerrel->relids))
+			return false;		/* it does reference innerrel */
+	}
+
+	return true;
+}
+
 
 /*
  * Remove the target relid from the planner's data structures, having
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 8f9ae4f..00c746c 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -132,12 +132,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinType jointype, bool unique_inner);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinType jointype, bool unique_inner);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -152,7 +152,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinType jointype, bool unique_inner);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2189,7 +2189,8 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path->jointype,
+							  best_path->unique_inner);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->path);
 
@@ -2483,7 +2484,8 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   best_path->jpath.jointype,
+							   best_path->jpath.unique_inner);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
@@ -2609,7 +2611,8 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  best_path->jpath.jointype,
+							  best_path->jpath.unique_inner);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3714,7 +3717,8 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinType jointype,
+			  bool unique_inner)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3725,6 +3729,7 @@ make_nestloop(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->join.jointype = jointype;
+	node->join.unique_inner = unique_inner;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
 
@@ -3738,7 +3743,8 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinType jointype,
+			  bool unique_inner)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3750,6 +3756,7 @@ make_hashjoin(List *tlist,
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
 	node->join.jointype = jointype;
+	node->join.unique_inner = unique_inner;
 	node->join.joinqual = joinclauses;
 
 	return node;
@@ -3798,7 +3805,8 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinType jointype,
+			   bool unique_inner)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3814,6 +3822,7 @@ make_mergejoin(List *tlist,
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
 	node->join.jointype = jointype;
+	node->join.unique_inner = unique_inner;
 	node->join.joinqual = joinclauses;
 
 	return node;
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 93484a0..b794d8e 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -175,6 +175,12 @@ query_planner(PlannerInfo *root, List *tlist,
 	fix_placeholder_input_needed_levels(root);
 
 	/*
+	 * Analyze all joins and mark which ones can produce at most 1 row
+	 * for each outer row.
+	 */
+	mark_unique_joins(root, joinlist);
+
+	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
 	 * until we've built baserel data structures and classified qual clauses.
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 319e8b2..e6bb11f 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1722,6 +1722,7 @@ create_nestloop_path(PlannerInfo *root,
 								  &restrict_clauses);
 	pathnode->path.pathkeys = pathkeys;
 	pathnode->jointype = jointype;
+	pathnode->unique_inner = sjinfo->unique_inner;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
@@ -1779,6 +1780,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  &restrict_clauses);
 	pathnode->jpath.path.pathkeys = pathkeys;
 	pathnode->jpath.jointype = jointype;
+	pathnode->jpath.unique_inner = sjinfo->unique_inner;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
@@ -1847,6 +1849,7 @@ create_hashjoin_path(PlannerInfo *root,
 	 */
 	pathnode->jpath.path.pathkeys = NIL;
 	pathnode->jpath.jointype = jointype;
+	pathnode->jpath.unique_inner = sjinfo->unique_inner;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 41b13b2..c0192b7 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1560,6 +1560,7 @@ typedef struct JoinState
 {
 	PlanState	ps;
 	JoinType	jointype;
+	bool		unique_inner;	/* Inner side will match 0 or 1 outer rows */
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
 } JoinState;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 48203a0..61a1898 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -524,9 +524,12 @@ typedef struct CustomScan
 /* ----------------
  *		Join node
  *
- * jointype:	rule for joining tuples from left and right subtrees
- * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
- *				(plan.qual contains conditions that came from WHERE)
+ * jointype:		rule for joining tuples from left and right subtrees
+ * unique_inner:	true if the planner discovered that each inner tuple
+ *					can only match at most 1 outer tuple. Proofs include
+ *					UNIQUE indexes and GROUP BY/DISTINCT clauses etc.
+ * joinqual:		qual conditions that came from JOIN/ON or JOIN/USING
+ *					(plan.qual contains conditions that came from WHERE)
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -541,6 +544,7 @@ typedef struct Join
 {
 	Plan		plan;
 	JoinType	jointype;
+	bool		unique_inner;	/* Inner side will match 0 or 1 outer rows */
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
 } Join;
 
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 7116496..da28b46 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1028,6 +1028,8 @@ typedef struct JoinPath
 
 	JoinType	jointype;
 
+	bool		unique_inner;	/* inner side of join is unique */
+
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
 
@@ -1404,6 +1406,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		unique_inner;	/* inner side is unique on join condition */
 	List	   *join_quals;		/* join quals, in implicit-AND list format */
 } SpecialJoinInfo;
 
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 3fdc2cb..c4710de 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -121,6 +121,7 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
#2David Rowley
dgrowleyml@gmail.com
In reply to: David Rowley (#1)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 1 January 2015 at 02:47, David Rowley <dgrowleyml@gmail.com> wrote:

Hi,

I've been hacking a bit at the join code again... This time I've been
putting some effort into optimising the case where the inner side of the
join is known to be unique.
For example, given the tables:

create table t1 (id int primary key);
create table t2 (id int primary key);

And query such as:

select * from t1 left outer join t2 on t1.id=t2.id;

It is possible to deduce at planning time that "for each row in the outer
relation, only 0 or 1 rows can exist in the inner relation", (inner being
t2)

I've been hacking at this unique join idea again and I've now got it
working for all join types -- Patch attached.

Here's how the performance is looking:

postgres=# create table t1 (id int primary key);
CREATE TABLE
postgres=# create table t2 (id int primary key);
CREATE TABLE
postgres=# insert into t1 select x.x from generate_series(1,1000000) x(x);
INSERT 0 1000000
postgres=# insert into t2 select x.x from generate_series(1,1000000) x(x);
INSERT 0 1000000
postgres=# vacuum analyze;
VACUUM
postgres=# \q

Query: select count(t1.id) from t1 inner join t2 on t1.id=t2.id;

With Patch on master as of 32bf6ee

D:\Postgres\install\bin>pgbench -f d:\unijoin3.sql -T 60 -n postgres
transaction type: Custom query
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
duration: 60 s
number of transactions actually processed: 78
latency average: 769.231 ms
tps = 1.288260 (including connections establishing)
tps = 1.288635 (excluding connections establishing)

Master as of 32bf6ee

D:\Postgres\install\bin>pgbench -f d:\unijoin3.sql -T 60 -n postgres
transaction type: Custom query
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
duration: 60 s
number of transactions actually processed: 70
latency average: 857.143 ms
tps = 1.158905 (including connections establishing)
tps = 1.159264 (excluding connections establishing)

That's a 10% performance increase.

I still need to perform more thorough benchmarking with different data
types.

One weird thing that I noticed before is that in an earlier revision of the
patch in the executor's join Initialise node code, I had set the
unique_inner to true for semi joins and replaced the SEMI_JOIN check for a
unique_join check in the execute node for each join method. With this the
performance results barely changed from standard... I've yet to find out
why.

The patch also has added a property to the EXPLAIN (VERBOSE) output which
states if the join was found to be unique or not.

The patch also still requires a final pass of comment fix-ups. I've just
plain run out of time for now.

I'll pick this up in 2 weeks time.

Regards

David Rowley

Attachments:

unijoin_2015-01-31_9af9cfa.patchapplication/octet-stream; name=unijoin_2015-01-31_9af9cfa.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7cfc9bb..a2d5f5b 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1269,10 +1269,27 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->format == EXPLAIN_FORMAT_TEXT)
 		appendStringInfoChar(es->str, '\n');
 
-	/* target list */
 	if (es->verbose)
+	{
+		/* target list */
 		show_plan_tlist(planstate, ancestors, es);
 
+		/* unique join */
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *)plan)->unique_inner ? "Yes" : "No";
+				ExplainPropertyText("Unique Join", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 1d78cdf..28fc618 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semijoin or if the planner found that no more than
+					 * 1 inner row will match a single outer row, we'll
+					 * consider returning the first match, but after that we're
+					 * done with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI || node->js.unique_inner)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -469,6 +471,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 		ExecInitExpr((Expr *) node->join.plan.qual,
 					 (PlanState *) hjstate);
 	hjstate->js.jointype = node->join.jointype;
+	hjstate->js.unique_inner = node->join.unique_inner;
 	hjstate->js.joinqual = (List *)
 		ExecInitExpr((Expr *) node->join.joinqual,
 					 (PlanState *) hjstate);
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 15742c5..5ee0970 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,12 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semijoin or if the planner found that no more than
+					 * 1 inner row will match a single outer row, we'll
+					 * consider returning the first match, but after that we're
+					 * done with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI || node->js.unique_inner)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1511,6 +1513,7 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 		ExecInitExpr((Expr *) node->join.plan.qual,
 					 (PlanState *) mergestate);
 	mergestate->js.jointype = node->join.jointype;
+	mergestate->js.unique_inner = node->join.unique_inner;
 	mergestate->js.joinqual = (List *)
 		ExecInitExpr((Expr *) node->join.joinqual,
 					 (PlanState *) mergestate);
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e66bcda..a31e1d5 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,11 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * In a semijoin or if the planner found that no more than 1 inner
+			 * row will match a single outer row, we'll consider returning the
+			 * first match, but after that we're done with this outer tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.jointype == JOIN_SEMI || node->js.unique_inner)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -327,6 +328,7 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 		ExecInitExpr((Expr *) node->join.plan.qual,
 					 (PlanState *) nlstate);
 	nlstate->js.jointype = node->join.jointype;
+	nlstate->js.unique_inner = node->join.unique_inner;
 	nlstate->js.joinqual = (List *)
 		ExecInitExpr((Expr *) node->join.joinqual,
 					 (PlanState *) nlstate);
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index f1a24f5..9ef453e 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -639,6 +639,7 @@ CopyJoinFields(const Join *from, Join *newnode)
 	CopyPlanFields((const Plan *) from, (Plan *) newnode);
 
 	COPY_SCALAR_FIELD(jointype);
+	COPY_SCALAR_FIELD(unique_inner);
 	COPY_NODE_FIELD(joinqual);
 }
 
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 020558b..96ea8c5 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2620,13 +2620,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		path->jpath.unique_inner)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * SEMI, ANTI joins and joins that the planner found to have a unique
+		 * inner side will stop after first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index eb65c97..f3cf3cb 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -49,7 +49,7 @@ static List *generate_join_implied_equalities_broken(PlannerInfo *root,
 										Relids outer_relids,
 										Relids nominal_inner_relids,
 										RelOptInfo *inner_rel);
-static Oid select_equality_operator(EquivalenceClass *ec,
+Oid select_equality_operator(EquivalenceClass *ec,
 						 Oid lefttype, Oid righttype);
 static RestrictInfo *create_join_clause(PlannerInfo *root,
 				   EquivalenceClass *ec, Oid opno,
@@ -1282,7 +1282,7 @@ generate_join_implied_equalities_broken(PlannerInfo *root,
  *
  * Returns InvalidOid if no operator can be found for this datatype combination
  */
-static Oid
+Oid
 select_equality_operator(EquivalenceClass *ec, Oid lefttype, Oid righttype)
 {
 	ListCell   *lc;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 11d3933..5c7e9ec 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -32,12 +32,47 @@
 #include "utils/lsyscache.h"
 
 /* local functions */
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo, RelOptInfo **uniquerel);
+static bool eclassjoin_is_unique_join(PlannerInfo *root, List *joinlist,
+					  RangeTblRef *candidatertr);
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, joinlist)
+	{
+		RangeTblRef		*rtr = (RangeTblRef *) lfirst(lc);
+		bool			 uniquejoin;
+
+		if (!IsA(rtr, RangeTblRef))
+			continue;
+
+		uniquejoin = eclassjoin_is_unique_join(root, joinlist, rtr);
+
+		root->simple_rel_array[rtr->rtindex]->has_unique_join = uniquejoin;
+	}
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+		RelOptInfo		*uniquerel;
+
+		/*
+		 * Determine if the inner side of the join can produce at most 1 row
+		 * for any single outer row.
+		 */
+		if (specialjoin_is_unique_join(root, sjinfo, &uniquerel))
+			uniquerel->has_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -91,6 +126,16 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * The removal of the join could have caused push down quals to
+		 * have been removed, which could previously have stopped us
+		 * from marking the join as a unique join. We'd better make
+		 * another pass over each join to make sure otherwise we may fail
+		 * to remove other joins which had a join condition on the join
+		 * that we just removed.
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -151,18 +196,12 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
-	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
-	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
-		sjinfo->delay_upper_joins)
+	/* We can only remove LEFT JOINs */
+	if (sjinfo->jointype != JOIN_LEFT)
 		return false;
 
 	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
@@ -170,38 +209,11 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
+	/* only joins that don't duplicate their outer side can be removed */
+	if (!innerrel->has_unique_join)
 		return false;
 
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -253,6 +265,254 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+static bool
+eclassjoin_is_unique_join(PlannerInfo *root, List *joinlist, RangeTblRef *candidatertr)
+{
+	ListCell   *lc;
+	Query	   *subquery = NULL;
+	RelOptInfo *candidaterel;
+
+	candidaterel = find_base_rel(root, candidatertr->rtindex);
+
+	/*
+	 * The only relation type that can help us is a base rel. If the relation
+	 * does not have any eclass joins then we'll assume that it's not an
+	 * equality type join, therefore won't be provably unique.
+	 */
+	if (candidaterel->reloptkind != RELOPT_BASEREL ||
+		candidaterel->has_eclass_joins == false)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the rel.
+	 */
+	if (candidaterel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation rel, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's no point in going
+		 * further.
+		 */
+		if (candidaterel->indexlist == NIL)
+			return false;
+	}
+	else if (candidaterel->rtekind == RTE_SUBQUERY)
+	{
+		subquery = root->simple_rte_array[candidatertr->rtindex]->subquery;
+
+		/*
+		 * If the subquery has no qualities that support distinctness proofs
+		 * then there's no point in going further.
+		 */
+		if (!query_supports_distinctness(subquery))
+			return false;
+	}
+	else
+		return false;			/* unsupported rtekind */
+
+	/*
+	 * We now search through each relation in the join list and find every
+	 * join condition column which joins to this rel. In order for the join
+	 * to be marked as unique, a unique index must exist on this relation
+	 * for the join conditions of each relation joining to this relation.
+	 */
+	foreach (lc, joinlist)
+	{
+		RangeTblRef	*rtr = (RangeTblRef *) lfirst(lc);
+		RelOptInfo	*rel;
+		ListCell	*lc2;
+		List		*index_vars;
+		List		*operator_list;
+
+		/* we can't remove ourself, or anything other than RangeTblRefs */
+		if (rtr == candidatertr || !IsA(rtr, RangeTblRef))
+			continue;
+
+		rel = find_base_rel(root, rtr->rtindex);
+
+		/*
+		 * The only relation type that can help us is a base rel with at least
+		 * one foreign key defined, if there's no eclass joins then this rel
+		 * is not going to help us prove the removalrel is not needed.
+		 */
+		if (rel->reloptkind != RELOPT_BASEREL ||
+			rel->rtekind != RTE_RELATION ||
+			rel->has_eclass_joins == false)
+			continue;
+
+		/*
+		 * Both rels have eclass joins, but do they have eclass joins to each
+		 * other? Skip this rel if it does not.
+		 */
+		if (!have_relevant_eclass_joinclause(root, rel, candidaterel))
+			continue;
+
+		index_vars = NIL;
+		operator_list = NIL;
+
+		/* now populate the lists with the join condition Vars from rel */
+		foreach(lc2, root->eq_classes)
+		{
+			EquivalenceClass *ec = (EquivalenceClass *) lfirst(lc2);
+
+			if (list_length(ec->ec_members) <= 1)
+				continue;
+
+			if (bms_overlap(candidaterel->relids, ec->ec_relids) &&
+				bms_overlap(rel->relids, ec->ec_relids))
+			{
+				ListCell *lc3;
+				Var *relvar = NULL;
+				Var *candidaterelvar = NULL;
+
+				foreach (lc3, ec->ec_members)
+				{
+					EquivalenceMember *ecm = (EquivalenceMember *) lfirst(lc3);
+
+					Var *var = (Var *) ecm->em_expr;
+
+					if (!IsA(var, Var))
+						continue; /* Ignore Consts */
+
+					if (var->varno == rel->relid)
+					{
+						if (relvar != NULL)
+							return false;
+						relvar = var;
+					}
+
+					else if (var->varno == candidaterel->relid)
+					{
+						if (candidaterelvar != NULL)
+							return false;
+						candidaterelvar = var;
+					}
+				}
+
+				if (relvar != NULL && candidaterelvar != NULL)
+				{
+					Oid opno;
+
+					/* grab the correct equality operator for these two vars */
+					opno = select_equality_operator(ec, relvar->vartype, candidaterelvar->vartype);
+
+					if (!OidIsValid(opno))
+						return false;
+
+					index_vars = lappend(index_vars, candidaterelvar);
+					operator_list = lappend_oid(operator_list, opno);
+				}
+			}
+		}
+
+		Assert(index_vars != NIL);
+
+		if (candidaterel->rtekind == RTE_RELATION)
+		{
+			if (!relation_has_unique_index_for(root, candidaterel, NIL, index_vars, operator_list))
+				return false;
+		}
+		else
+		{
+			List	   *colnos = NIL;
+			ListCell   *l;
+
+			/*
+			 * Build the argument lists for query_is_distinct_for: a list of
+			 * output column numbers that the query needs to be distinct over.
+			 */
+			foreach(l, index_vars)
+			{
+				Var		   *var = (Var *) lfirst(l);
+
+				colnos = lappend_int(colnos, var->varattno);
+			}
+
+			if (!query_is_distinct_for(subquery, colnos, operator_list))
+				return false;
+
+		}
+	}
+
+	return true;
+}
+
+/*
+ * Returns True if it can be proved that this special join will never produce
+ * more than 1 row per outer row, otherwise returns false if there is
+ * insufficient evidence to prove the join is unique. uniquerel is always
+ * set to the RelOptInfo of the rel that was proved to be unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo,
+						   RelOptInfo **uniquerel)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Query	   *subquery = NULL;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's any delayed upper joins then we'll need to punt. */
+	if (sjinfo->delay_upper_joins)
+		return false;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	/* check if we've already marked this join as unique on a previous call */
+	if (innerrel->has_unique_join)
+	{
+		*uniquerel = innerrel;
+		return true;
+	}
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (innerrel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation innerrel, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's no point in going
+		 * further.
+		 */
+		if (innerrel->indexlist == NIL)
+			return false;
+	}
+	else if (innerrel->rtekind == RTE_SUBQUERY)
+	{
+		subquery = root->simple_rte_array[innerrelid]->subquery;
+
+		/*
+		 * If the subquery has no qualities that support distinctness proofs
+		 * then there's no point in going further.
+		 */
+		if (!query_supports_distinctness(subquery))
+			return false;
+	}
+	else
+		return false;			/* unsupported rtekind */
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -310,7 +570,10 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	{
 		/* Now examine the indexes to see if we have a matching unique index */
 		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
+		{
+			*uniquerel = innerrel;
 			return true;
+		}
 	}
 	else	/* innerrel->rtekind == RTE_SUBQUERY */
 	{
@@ -358,7 +621,10 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		}
 
 		if (query_is_distinct_for(subquery, colnos, opids))
+		{
+			*uniquerel = innerrel;
 			return true;
+		}
 	}
 
 	/*
@@ -368,7 +634,6 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	return false;
 }
 
-
 /*
  * Remove the target relid from the planner's data structures, having
  * determined that there is no need to include it in the query.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 655be81..e6f247e 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -132,12 +132,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinType jointype, bool unique_inner);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinType jointype, bool unique_inner);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -152,7 +152,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinType jointype, bool unique_inner);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2189,7 +2189,8 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path->jointype,
+							  best_path->unique_inner);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->path);
 
@@ -2483,7 +2484,8 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   best_path->jpath.jointype,
+							   best_path->jpath.unique_inner);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
@@ -2609,7 +2611,8 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  best_path->jpath.jointype,
+							  best_path->jpath.unique_inner);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3714,7 +3717,8 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinType jointype,
+			  bool unique_inner)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3725,6 +3729,7 @@ make_nestloop(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->join.jointype = jointype;
+	node->join.unique_inner = unique_inner;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
 
@@ -3738,7 +3743,8 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinType jointype,
+			  bool unique_inner)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3750,6 +3756,7 @@ make_hashjoin(List *tlist,
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
 	node->join.jointype = jointype;
+	node->join.unique_inner = unique_inner;
 	node->join.joinqual = joinclauses;
 
 	return node;
@@ -3798,7 +3805,8 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinType jointype,
+			   bool unique_inner)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3814,6 +3822,7 @@ make_mergejoin(List *tlist,
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
 	node->join.jointype = jointype;
+	node->join.unique_inner = unique_inner;
 	node->join.joinqual = joinclauses;
 
 	return node;
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 848df97..9fcd047 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -175,6 +175,12 @@ query_planner(PlannerInfo *root, List *tlist,
 	fix_placeholder_input_needed_levels(root);
 
 	/*
+	 * Analyze all joins and mark which ones can produce at most 1 row
+	 * for each outer row.
+	 */
+	mark_unique_joins(root, joinlist);
+
+	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
 	 * until we've built baserel data structures and classified qual clauses.
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 1395a21..e7a867b 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1722,6 +1722,7 @@ create_nestloop_path(PlannerInfo *root,
 								  &restrict_clauses);
 	pathnode->path.pathkeys = pathkeys;
 	pathnode->jointype = jointype;
+	pathnode->unique_inner = inner_path->parent->has_unique_join;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
@@ -1779,6 +1780,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  &restrict_clauses);
 	pathnode->jpath.path.pathkeys = pathkeys;
 	pathnode->jpath.jointype = jointype;
+	pathnode->jpath.unique_inner = inner_path->parent->has_unique_join;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
@@ -1847,6 +1849,7 @@ create_hashjoin_path(PlannerInfo *root,
 	 */
 	pathnode->jpath.path.pathkeys = NIL;
 	pathnode->jpath.jointype = jointype;
+	pathnode->jpath.unique_inner = inner_path->parent->has_unique_join;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 41288ed..fcfca95 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1560,6 +1560,7 @@ typedef struct JoinState
 {
 	PlanState	ps;
 	JoinType	jointype;
+	bool		unique_inner;	/* Inner side will match 0 or 1 outer rows */
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
 } JoinState;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 316c9ce..d9af1e5 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -524,9 +524,12 @@ typedef struct CustomScan
 /* ----------------
  *		Join node
  *
- * jointype:	rule for joining tuples from left and right subtrees
- * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
- *				(plan.qual contains conditions that came from WHERE)
+ * jointype:		rule for joining tuples from left and right subtrees
+ * unique_inner:	true if the planner discovered that each inner tuple
+ *					can only match at most 1 outer tuple. Proofs include
+ *					UNIQUE indexes and GROUP BY/DISTINCT clauses etc.
+ * joinqual:		qual conditions that came from JOIN/ON or JOIN/USING
+ *					(plan.qual contains conditions that came from WHERE)
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -541,6 +544,7 @@ typedef struct Join
 {
 	Plan		plan;
 	JoinType	jointype;
+	bool		unique_inner;	/* Inner side will match 0 or 1 outer rows */
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
 } Join;
 
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 6845a40..7d127fa 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -470,6 +470,8 @@ typedef struct RelOptInfo
 	List	   *joininfo;		/* RestrictInfo structures for join clauses
 								 * involving this rel */
 	bool		has_eclass_joins;		/* T means joininfo is incomplete */
+	bool		has_unique_join;		/* True if this rel won't contain more
+										 * than 1 row for the join condition */
 } RelOptInfo;
 
 /*
@@ -1028,6 +1030,8 @@ typedef struct JoinPath
 
 	JoinType	jointype;
 
+	bool		unique_inner;	/* inner side of join is unique */
+
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
 
@@ -1404,6 +1408,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		unique_inner;	/* inner side is unique on join condition */
 	List	   *join_quals;		/* join quals, in implicit-AND list format */
 } SpecialJoinInfo;
 
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 6cad92e..74f3a1f 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -119,6 +119,8 @@ extern List *generate_join_implied_equalities(PlannerInfo *root,
 								 Relids join_relids,
 								 Relids outer_relids,
 								 RelOptInfo *inner_rel);
+extern Oid select_equality_operator(EquivalenceClass *ec, Oid lefttype,
+								 Oid righttype);
 extern bool exprs_known_equal(PlannerInfo *root, Node *item1, Node *item2);
 extern void add_child_rel_equivalences(PlannerInfo *root,
 						   AppendRelInfo *appinfo,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 082f7d7..0836fab 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -121,6 +121,7 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 2501184..754d8a0 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3092,6 +3092,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Unique Join: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3099,12 +3100,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Unique Join: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3191,7 +3193,7 @@ select id from a where id in (
 );
          QUERY PLAN         
 ----------------------------
- Hash Semi Join
+ Hash Join
    Hash Cond: (a.id = b.id)
    ->  Seq Scan on a
    ->  Hash
@@ -3907,12 +3909,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -3939,12 +3942,13 @@ select * from
 ----------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, 42::bigint))
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, 42::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -3972,6 +3976,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Unique Join: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -3979,7 +3984,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -3999,12 +4004,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Unique Join: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4026,10 +4032,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Unique Join: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Unique Join: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4038,7 +4046,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4084,10 +4092,12 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Unique Join: No
          Join Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
@@ -4095,7 +4105,7 @@ select * from
                Output: c.q1
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1
-(13 rows)
+(15 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -4162,13 +4172,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, 42::bigint)), d.q1, (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, 42::bigint)), d.q2)))
+   Unique Join: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, 42::bigint)), (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2))
+         Unique Join: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, 42::bigint)), (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2))
+               Unique Join: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, 42::bigint))
+                     Unique Join: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -4184,7 +4198,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -4202,17 +4216,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Unique Join: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Unique Join: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Unique Join: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Unique Join: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Unique Join: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -4234,7 +4253,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -4247,16 +4266,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Unique Join: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Unique Join: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- test some error cases where LATERAL should have been used but wasn't
 select f1,g from int4_tbl a, (select f1 as g) ss;
@@ -4339,3 +4360,261 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Unique Join: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Join: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Unique Join: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Join: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Join: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Join: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Unique Join: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure distinct clause uniquifies the join 
+explain (verbose, costs off)
+select * from j1 
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Join: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  HashAggregate
+         Output: j3.id
+         Group Key: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(11 rows)
+
+-- ensure group by clause uniquifies the join 
+explain (verbose, costs off)
+select * from j1 
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Join: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  HashAggregate
+         Output: j3.id
+         Group Key: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(11 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Unique Join: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Join: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Join: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure no unique joins when the tables are joined in 3 ways.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id2 and j1.id2 = j2.id2
+inner join j3 on j1.id1 = j3.id1 and j2.id2 = j3.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2, j3.id1, j3.id2
+   Unique Join: No
+   Join Filter: (j1.id1 = j3.id1)
+   ->  Nested Loop
+         Output: j1.id1, j1.id2, j2.id1, j2.id2
+         Unique Join: No
+         Join Filter: (j1.id1 = j2.id2)
+         ->  Seq Scan on public.j1
+               Output: j1.id1, j1.id2
+               Filter: (j1.id1 = j1.id2)
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j3
+         Output: j3.id1, j3.id2
+         Filter: (j3.id1 = j3.id2)
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 7991e99..99feee4 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2016,12 +2016,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2042,11 +2043,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index b14410f..7f43784 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -761,6 +761,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Unique Join: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -769,7 +770,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -789,6 +790,7 @@ select * from int4_tbl o where (f1, f1) in
 ----------------------------------------------------------------------
  Hash Join
    Output: o.f1
+   Unique Join: No
    Hash Cond: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -805,7 +807,7 @@ select * from int4_tbl o where (f1, f1) in
                            Group Key: i.f1
                            ->  Seq Scan on public.int4_tbl i
                                  Output: i.f1
-(18 rows)
+(19 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 80c5706..1c790ac 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2065,6 +2065,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
          Filter: snoop(t1.a)
          ->  Nested Loop Semi Join
                Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c
+               Unique Join: No
                ->  Seq Scan on public.t1 t1_5
                      Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c
                      Filter: ((t1_5.a > 5) AND (t1_5.a = 3) AND leakproof(t1_5.a))
@@ -2080,6 +2081,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
          Filter: snoop(t1_1.a)
          ->  Nested Loop Semi Join
                Output: t11.ctid, t11.a, t11.b, t11.c, t11.d
+               Unique Join: No
                ->  Seq Scan on public.t11
                      Output: t11.ctid, t11.a, t11.b, t11.c, t11.d
                      Filter: ((t11.a > 5) AND (t11.a = 3) AND leakproof(t11.a))
@@ -2095,6 +2097,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
          Filter: snoop(t1_2.a)
          ->  Nested Loop Semi Join
                Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e
+               Unique Join: No
                ->  Seq Scan on public.t12 t12_2
                      Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e
                      Filter: ((t12_2.a > 5) AND (t12_2.a = 3) AND leakproof(t12_2.a))
@@ -2110,6 +2113,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
          Filter: snoop(t1_3.a)
          ->  Nested Loop Semi Join
                Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e
+               Unique Join: No
                ->  Seq Scan on public.t111 t111_3
                      Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                      Filter: ((t111_3.a > 5) AND (t111_3.a = 3) AND leakproof(t111_3.a))
@@ -2120,7 +2124,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                      ->  Seq Scan on public.t111 t111_4
                            Output: t111_4.a
                            Filter: (t111_4.a = 3)
-(61 rows)
+(65 rows)
 
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
 SELECT * FROM v1 WHERE a=100; -- Nothing should have been changed to 100
@@ -2143,6 +2147,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
          Filter: snoop(t1.a)
          ->  Nested Loop Semi Join
                Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c
+               Unique Join: No
                ->  Seq Scan on public.t1 t1_5
                      Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c
                      Filter: ((t1_5.a > 5) AND (t1_5.a = 8) AND leakproof(t1_5.a))
@@ -2158,6 +2163,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
          Filter: snoop(t1_1.a)
          ->  Nested Loop Semi Join
                Output: t11.a, t11.ctid, t11.b, t11.c, t11.d
+               Unique Join: No
                ->  Seq Scan on public.t11
                      Output: t11.a, t11.ctid, t11.b, t11.c, t11.d
                      Filter: ((t11.a > 5) AND (t11.a = 8) AND leakproof(t11.a))
@@ -2173,6 +2179,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
          Filter: snoop(t1_2.a)
          ->  Nested Loop Semi Join
                Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e
+               Unique Join: No
                ->  Seq Scan on public.t12 t12_2
                      Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e
                      Filter: ((t12_2.a > 5) AND (t12_2.a = 8) AND leakproof(t12_2.a))
@@ -2188,6 +2195,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
          Filter: snoop(t1_3.a)
          ->  Nested Loop Semi Join
                Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e
+               Unique Join: No
                ->  Seq Scan on public.t111 t111_3
                      Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                      Filter: ((t111_3.a > 5) AND (t111_3.a = 8) AND leakproof(t111_3.a))
@@ -2198,7 +2206,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                      ->  Seq Scan on public.t111 t111_4
                            Output: t111_4.a
                            Filter: (t111_4.a = 8)
-(61 rows)
+(65 rows)
 
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
 NOTICE:  snooped value: 8
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 06b372b..4e2fe1f 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2093,6 +2093,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: 42::bigint, 47::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Unique Join: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2100,6 +2101,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Unique Join: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2107,6 +2109,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Unique Join: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2114,12 +2117,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Unique Join: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(34 rows)
+(38 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 718e1d9..687f848 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1267,3 +1267,99 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+-- ensure no unique joins when the tables are joined in 3 ways.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id2 and j1.id2 = j2.id2
+inner join j3 on j1.id1 = j3.id1 and j2.id2 = j3.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#3Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: David Rowley (#2)
Re: Performance improvement for joins where outer side is unique

Hi, I had a look on this patch. Although I haven't understood
whole the stuff and all of the related things, I will comment as
possible.

Performance:

I looked on the performance gain this patch gives. For several
on-memory joins, I had gains about 3% for merge join, 5% for hash
join, and 10% for nestloop (@CentOS6), for simple 1-level joins
with aggregation similar to what you mentioned in previous
mail like this.

=# SELECT count(*) FROM t1 JOIN t2 USING (id);

Of course, the gain would be trivial when returning many tuples,
or scans go to disk.

I haven't measured the loss by additional computation when the
query is regarded as not a "single join".

Explain representation:

I don't like that the 'Unique Join:" occupies their own lines in
the result of explain, moreover, it doesn't show the meaning
clearly. What about the representation like the following? Or,
It might not be necessary to appear there.

   Nested Loop ...
     Output: ....
 -   Unique Jion: Yes
       -> Seq Scan on public.t2 (cost = ...
 -     -> Seq Scan on public.t1 (cost = ....
 +     -> Seq Scan on public.t1 (unique) (cost = ....

Coding:

The style looks OK. Could applied on master.
It looks to work as you expected for some cases.

Random comments follow.

- Looking specialjoin_is_unique_join, you seem to assume that
!delay_upper_joins is a sufficient condition for not being
"unique join". The former simplly indicates that "don't
commute with upper OJs" and the latter seems to indicate that
"the RHS is unique", Could you tell me what kind of
relationship is there between them?

- The naming "unique join" seems a bit obscure for me, but I
don't have no better idea:( However, the member name
"has_unique_join" seems to be better to be "is_unique_join".

- eclassjoin_is_unique_join involves seemingly semi-exhaustive
scan on ec members for every element in joinlist. Even if not,
it might be thought to be a bit too large for the gain. Could
you do the equivelant things by more small code?

regards,

Jan 2015 00:37:19 +1300, David Rowley <dgrowleyml@gmail.com> wrote in <CAApHDvod_uCMoUPovdpXbNkw50O14x3wwKoJmZLxkbBn71zdEg@mail.gmail.com>

On 1 January 2015 at 02:47, David Rowley <dgrowleyml@gmail.com> wrote:

Hi,

I've been hacking a bit at the join code again... This time I've been
putting some effort into optimising the case where the inner side of the
join is known to be unique.
For example, given the tables:

create table t1 (id int primary key);
create table t2 (id int primary key);

And query such as:

select * from t1 left outer join t2 on t1.id=t2.id;

It is possible to deduce at planning time that "for each row in the outer
relation, only 0 or 1 rows can exist in the inner relation", (inner being
t2)

I've been hacking at this unique join idea again and I've now got it
working for all join types -- Patch attached.

Here's how the performance is looking:

postgres=# create table t1 (id int primary key);
CREATE TABLE
postgres=# create table t2 (id int primary key);
CREATE TABLE
postgres=# insert into t1 select x.x from generate_series(1,1000000) x(x);
INSERT 0 1000000
postgres=# insert into t2 select x.x from generate_series(1,1000000) x(x);
INSERT 0 1000000
postgres=# vacuum analyze;
VACUUM
postgres=# \q

Query: select count(t1.id) from t1 inner join t2 on t1.id=t2.id;

With Patch on master as of 32bf6ee

D:\Postgres\install\bin>pgbench -f d:\unijoin3.sql -T 60 -n postgres
transaction type: Custom query
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
duration: 60 s
number of transactions actually processed: 78
latency average: 769.231 ms
tps = 1.288260 (including connections establishing)
tps = 1.288635 (excluding connections establishing)

Master as of 32bf6ee

D:\Postgres\install\bin>pgbench -f d:\unijoin3.sql -T 60 -n postgres
transaction type: Custom query
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
duration: 60 s
number of transactions actually processed: 70
latency average: 857.143 ms
tps = 1.158905 (including connections establishing)
tps = 1.159264 (excluding connections establishing)

That's a 10% performance increase.

I still need to perform more thorough benchmarking with different data
types.

One weird thing that I noticed before is that in an earlier revision of the
patch in the executor's join Initialise node code, I had set the
unique_inner to true for semi joins and replaced the SEMI_JOIN check for a
unique_join check in the execute node for each join method. With this the
performance results barely changed from standard... I've yet to find out
why.

I don't know even what you did precisely but if you do such a
thing, you perhaps should change the name of "unique_inner" to
something representing that it reads up to one row from the inner
for one outer row. (Sorry I have no good idea for this..)

The patch also has added a property to the EXPLAIN (VERBOSE) output which
states if the join was found to be unique or not.

The patch also still requires a final pass of comment fix-ups. I've just
plain run out of time for now.

I'll pick this up in 2 weeks time.

Regards

David Rowley

--
Kyotaro Horiguchi
NTT Open Source Software Center

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

#4Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: David Rowley (#2)
Re: Performance improvement for joins where outer side is unique

Hi David,

I've been looking at this patch, mostly because it seems like a great
starting point for improving estimation for joins on multi-column FKs.

Currently we do this:

CREATE TABLE parent (a INT, b INT, PRIMARY KEY (a,b));
CREATE TABLE child (a INT, b INT, FOREIGN KEY (a,b)
REFERENCES parent (a,b));

INSERT INTO parent SELECT i, i FROM generate_series(1,1000000) s(i);
INSERT INTO child SELECT i, i FROM generate_series(1,1000000) s(i);

ANALYZE;

EXPLAIN SELECT * FROM parent JOIN child USING (a,b);

QUERY PLAN
---------------------------------------------------------------------
Hash Join (cost=33332.00..66978.01 rows=1 width=8)
Hash Cond: ((parent.a = child.a) AND (parent.b = child.b))
-> Seq Scan on parent (cost=0.00..14425.00 rows=1000000 width=8)
-> Hash (cost=14425.00..14425.00 rows=1000000 width=8)
-> Seq Scan on child (cost=0.00..14425.00 rows=1000000 width=8)
(5 rows)

Which is of course non-sense, because we know it's a join on FK, so the
join will produce 1M rows (just like the child table).

This seems like a rather natural extension of what you're doing in this
patch, except that it only affects the optimizer and not the executor.
Do you have any plans in this direction? If not, I'll pick this up as I
do have that on my TODO.

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

#5Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: David Rowley (#2)
Re: Performance improvement for joins where outer side is unique

Hi,

I tried to do an initdb with the patch applied, and seems there's a bug
somewhere in analyzejoins.c:

tomas@rimmer ~ $ pg_ctl -D tmp/pg-unidata init
The files belonging to this database system will be owned by user "tomas".
This user must also own the server process.

The database cluster will be initialized with locale "en_US".
The default database encoding has accordingly been set to "LATIN1".
The default text search configuration will be set to "english".

Data page checksums are disabled.

creating directory tmp/pg-unidata ... ok
creating subdirectories ... ok
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting dynamic shared memory implementation ... sysv
creating configuration files ... ok
creating template1 database in tmp/pg-unidata/base/1 ... ok
initializing pg_authid ... ok
initializing dependencies ... ok
creating system views ... ok
loading system objects' descriptions ... TRAP:
FailedAssertion("!(index_vars != ((List *) ((void *)0)))", File:
"analyzejoins.c", Line: 414)
sh: line 1: 339 Aborted
"/home/tomas/pg-unijoins/bin/postgres" --single -F -O -c
search_path=pg_catalog -c exit_on_error=true template1 > /dev/null
child process exited with exit code 134
initdb: removing data directory "tmp/pg-unidata"
pg_ctl: database system initialization failed

The problem seems to be the last command in setup_description() at
src/bin/initdb/initdb.c:1843, i.e. this query:

WITH funcdescs AS (
SELECT p.oid as p_oid, oprname,
coalesce(obj_description(o.oid, 'pg_operator'),'') as opdesc
FROM pg_proc p JOIN pg_operator o ON oprcode = p.oid )
INSERT INTO pg_description
SELECT p_oid, 'pg_proc'::regclass, 0,
'implementation of ' || oprname || ' operator'
FROM funcdescs
WHERE opdesc NOT LIKE 'deprecated%' AND
NOT EXISTS (SELECT 1 FROM pg_description
WHERE objoid = p_oid AND classoid = 'pg_proc'::regclass)
And particularly the join in the CTE, i.e. this fails

SELECT * FROM pg_proc p JOIN pg_operator o ON oprcode = p.oid

I'm not quite sure why, but eclassjoin_is_unique_join() never actually
jumps into this part (line ~400):

if (relvar != NULL && candidaterelvar != NULL)
{
...
index_vars = lappend(index_vars, candidaterelvar);
...
}

so the index_vars is NIL. Not sure why, but I'm sure you'll spot the
issue right away.

BTW, I find this coding (first cast, then check) rather strange:

Var *var = (Var *) ecm->em_expr;

if (!IsA(var, Var))
continue; /* Ignore Consts */

It's probably harmless, but I find it confusing and I can't remember
seeing it elsewhere in the code (for example clausesel.c and such) use
this style:

... clause is (Node*) ...

if (IsA(clause, Var))
{
Var *var = (Var*)clause;
...
}

or

Var * var = NULL;

if (! IsA(clause, Var))
// error / continue

var = (Var*)clause;

--
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

#6Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tomas Vondra (#5)
Re: Performance improvement for joins where outer side is unique

On 26.2.2015 01:15, Tomas Vondra wrote:

I'm not quite sure why, but eclassjoin_is_unique_join() never actually
jumps into this part (line ~400):

if (relvar != NULL && candidaterelvar != NULL)
{
...
index_vars = lappend(index_vars, candidaterelvar);
...
}

so the index_vars is NIL. Not sure why, but I'm sure you'll spot the
issue right away.

FWIW this apparently happens because the code only expect that
EquivalenceMembers only contain Var, but in this particular case that's
not the case - it contains RelabelType (because oprcode is regproc, and
needs to be relabeled to oid).

Adding this before the IsA(var, Var) check fixes the issue

if (IsA(var, RelabelType))
var = (Var*) ((RelabelType *) var)->arg;

but this makes the code even more confusing, because 'var' suggests it's
a Var node, but it's RelabelType node.

Also, specialjoin_is_unique_join() may have the same problem, but I've
been unable to come up with an example.

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

#7Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tomas Vondra (#6)
Re: Performance improvement for joins where outer side is unique

Tomas Vondra <tomas.vondra@2ndquadrant.com> writes:

FWIW this apparently happens because the code only expect that
EquivalenceMembers only contain Var, but in this particular case that's
not the case - it contains RelabelType (because oprcode is regproc, and
needs to be relabeled to oid).

If it thinks an EquivalenceMember must be a Var, it's outright broken;
I'm pretty sure any nonvolatile expression is possible.

regards, tom lane

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

#8Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tom Lane (#7)
Re: Performance improvement for joins where outer side is unique

On 26.2.2015 18:34, Tom Lane wrote:

Tomas Vondra <tomas.vondra@2ndquadrant.com> writes:

FWIW this apparently happens because the code only expect that
EquivalenceMembers only contain Var, but in this particular case that's
not the case - it contains RelabelType (because oprcode is regproc, and
needs to be relabeled to oid).

If it thinks an EquivalenceMember must be a Var, it's outright
broken; I'm pretty sure any nonvolatile expression is possible.

I came to the same conclusion, because even with the RelabelType fix
it's trivial to crash it with a query like this:

SELECT 1 FROM pg_proc p JOIN pg_operator o
ON oprcode = (p.oid::int4 + 1);

I think fixing this (e.g. by restricting the optimization to Var-only
cases) should not be difficult, although it might be nice to handle
generic expressions too, and something like examine_variable() should do
the trick. But I think UNIQUE indexes on expressions are not all that
common.

--
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

#9David Rowley
dgrowleyml@gmail.com
In reply to: Kyotaro HORIGUCHI (#3)
Re: Performance improvement for joins where outer side is unique

On 3 February 2015 at 22:23, Kyotaro HORIGUCHI <
horiguchi.kyotaro@lab.ntt.co.jp> wrote:

Hi, I had a look on this patch. Although I haven't understood
whole the stuff and all of the related things, I will comment as
possible.

Great, thank you for taking the time to look and review the patch.

Performance:

I looked on the performance gain this patch gives. For several
on-memory joins, I had gains about 3% for merge join, 5% for hash
join, and 10% for nestloop (@CentOS6), for simple 1-level joins
with aggregation similar to what you mentioned in previous
mail like this.

=# SELECT count(*) FROM t1 JOIN t2 USING (id);

Of course, the gain would be trivial when returning many tuples,
or scans go to disk.

That's true, but joins where rows are filtered after the join condition
will win here too:

For example select * from t1 inner join t2 on t1.id=t2.id where t1.value >
t2.value;

Also queries with a GROUP BY clause. (Since grouping is always performed
after the join :-( )

I haven't measured the loss by additional computation when the

query is regarded as not a "single join".

I think this would be hard to measure, but likely if it is measurable then
you'd want to look at just planning time, rather than planning and
execution time.

Explain representation:

I don't like that the 'Unique Join:" occupies their own lines in
the result of explain, moreover, it doesn't show the meaning
clearly. What about the representation like the following? Or,
It might not be necessary to appear there.

Nested Loop ...
Output: ....
-   Unique Jion: Yes
-> Seq Scan on public.t2 (cost = ...
-     -> Seq Scan on public.t1 (cost = ....
+     -> Seq Scan on public.t1 (unique) (cost = ....

Yeah I'm not too big a fan of this either. I did at one evolution of the
patch I had "Unique Left Join" in the join node's line in the explain
output, but I hated that more and changed it just to be just in the VERBOSE
output, and after I did that I didn't want to change the join node's line
only when in verbose mode. I do quite like that it's a separate item for
the XML and JSON explain output. That's perhaps quite useful when the
explain output must be processed by software.

I'm totally open for better ideas on names, but I just don't have any at
the moment.

Coding:

The style looks OK. Could applied on master.
It looks to work as you expected for some cases.

Random comments follow.

- Looking specialjoin_is_unique_join, you seem to assume that
!delay_upper_joins is a sufficient condition for not being
"unique join". The former simplly indicates that "don't
commute with upper OJs" and the latter seems to indicate that
"the RHS is unique", Could you tell me what kind of
relationship is there between them?

The rationalisation around that are from the (now changed) version of
join_is_removable(), where the code read:

/*
* Must be a non-delaying left join to a single baserel, else we aren't
* going to be able to do anything with it.
*/
if (sjinfo->jointype != JOIN_LEFT ||
sjinfo->delay_upper_joins)
return false;

I have to admit that I didn't go and investigate why delayed upper joins
cannot be removed by left join removal code, I really just assumed that
we're just unable to prove that a join to such a relation won't match more
than one outer side's row. I kept this to maintain that behaviour as I
assumed it was there for a good reason.

- The naming "unique join" seems a bit obscure for me, but I
don't have no better idea:( However, the member name
"has_unique_join" seems to be better to be "is_unique_join".

Yeah, I agree with this. I just can't find something short enough that
means "based on the join condition, the inner side of the join will never
produce more than 1 row for any single outer row". Unique join was the best
I came up with. I'm totally open for better ideas.

I agree that is_unique_join is better than has_unique_join. I must have
just copied the has_eclass_joins struct member without thinking too hard
about it. I've now changed this in my local copy of the patch.

- eclassjoin_is_unique_join involves seemingly semi-exhaustive

scan on ec members for every element in joinlist. Even if not,
it might be thought to be a bit too large for the gain. Could
you do the equivelant things by more small code?

I'd imagine some very complex queries could have many equivalence classes
and members, though I hadn't really thought that this number would be so
great that processing here would suffer much. There's quite a few fast
paths out, like the test to ensure that both rels are mentioned
in ec_relids. Though for a query which is so complex to have a great number
of eclass' and members, likely there would be quite a few relations
involved and planning would be slower anyway. I'm not quite sure how else I
could write this and still find the unique join cases each time. We can't
really just give up half way through looking.

Thanks again for the review.

Regards

David Rowley

#10David Rowley
dgrowleyml@gmail.com
In reply to: Tomas Vondra (#4)
Re: Performance improvement for joins where outer side is unique

On 26 February 2015 at 08:39, Tomas Vondra <tomas.vondra@2ndquadrant.com>
wrote:

I've been looking at this patch, mostly because it seems like a great
starting point for improving estimation for joins on multi-column FKs.

Currently we do this:

CREATE TABLE parent (a INT, b INT, PRIMARY KEY (a,b));
CREATE TABLE child (a INT, b INT, FOREIGN KEY (a,b)
REFERENCES parent (a,b));

INSERT INTO parent SELECT i, i FROM generate_series(1,1000000) s(i);
INSERT INTO child SELECT i, i FROM generate_series(1,1000000) s(i);

ANALYZE;

EXPLAIN SELECT * FROM parent JOIN child USING (a,b);

QUERY PLAN
---------------------------------------------------------------------
Hash Join (cost=33332.00..66978.01 rows=1 width=8)
Hash Cond: ((parent.a = child.a) AND (parent.b = child.b))
-> Seq Scan on parent (cost=0.00..14425.00 rows=1000000 width=8)
-> Hash (cost=14425.00..14425.00 rows=1000000 width=8)
-> Seq Scan on child (cost=0.00..14425.00 rows=1000000 width=8)
(5 rows)

Which is of course non-sense, because we know it's a join on FK, so the
join will produce 1M rows (just like the child table).

This seems like a rather natural extension of what you're doing in this
patch, except that it only affects the optimizer and not the executor.
Do you have any plans in this direction? If not, I'll pick this up as I
do have that on my TODO.

Hi Tomas,

I guess similar analysis could be done on FKs as I'm doing on unique
indexes. Perhaps my patch for inner join removal can help you more with
that. You may notice that in this patch I have ended up changing the left
join removal code so that it just checks if has_unique_join is set for the
special join. Likely something similar could be done with your idea and the
inner join removals, just by adding some sort of flag on RelOptInfo to say
"join_row_exists" or some better name. Quite likely if there's any pending
foreign key triggers, then it won't matter at all for the sake of row
estimates.

Regards

David Rowley

#11David Rowley
dgrowleyml@gmail.com
In reply to: Tomas Vondra (#8)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 27 February 2015 at 06:48, Tomas Vondra <tomas.vondra@2ndquadrant.com>
wrote:

On 26.2.2015 18:34, Tom Lane wrote:

Tomas Vondra <tomas.vondra@2ndquadrant.com> writes:

FWIW this apparently happens because the code only expect that
EquivalenceMembers only contain Var, but in this particular case that's
not the case - it contains RelabelType (because oprcode is regproc, and
needs to be relabeled to oid).

If it thinks an EquivalenceMember must be a Var, it's outright
broken; I'm pretty sure any nonvolatile expression is possible.

I came to the same conclusion, because even with the RelabelType fix
it's trivial to crash it with a query like this:

SELECT 1 FROM pg_proc p JOIN pg_operator o
ON oprcode = (p.oid::int4 + 1);

Thanks for looking at this Tomas. Sorry it's taken this long for me to
respond, but I wanted to do so with a working patch.

I've made a few changes in the attached version:

1. Fixed Assert failure when eclass contained non-Var types, as reported by
you.
2. Added support for expression indexes.

The expression indexes should really be supported as with the previous
patch they worked ok with LEFT JOINs, but not INNER JOINs, that
inconsistency is pretty much a bug in my book, so I've fixed it.

The one weird quirk with the patch is that, if we had some tables like:

create table r1 (id int primary key, value int not null);
create table r2 (id int primary key);

And a query:
explain verbose select * from r1 inner join r2 on r1.id=r2.id where r2.id
=r1.value;

The join is not properly detected as a unique join. This is down to the
eclass containing 3 members, when the code finds the 2nd ec member for r1
it returns false as it already found another one. I'm not quite sure what
the fix is for this just yet as it's not quite clear to me how the code
would work if there were 2 vars from each relation in the same eclass... If
these Vars were of different types then which operators would we use for
them? I'm not sure if having eclassjoin_is_unique_join() append every
possible combination to index_exprs is the answer. I'm also not quite sure
if the complexity is worth the extra code either.

Updated patch attached.

Thank you for looking it and reporting that bug.

Regards

David Rowley

Attachments:

unijoin_2015-03-04_ac455bd.patchapplication/octet-stream; name=unijoin_2015-03-04_ac455bd.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a951c55..55a9505 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1268,10 +1268,27 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->format == EXPLAIN_FORMAT_TEXT)
 		appendStringInfoChar(es->str, '\n');
 
-	/* target list */
 	if (es->verbose)
+	{
+		/* target list */
 		show_plan_tlist(planstate, ancestors, es);
 
+		/* unique join */
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *)plan)->unique_inner ? "Yes" : "No";
+				ExplainPropertyText("Unique Join", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 1d78cdf..28fc618 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semijoin or if the planner found that no more than
+					 * 1 inner row will match a single outer row, we'll
+					 * consider returning the first match, but after that we're
+					 * done with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI || node->js.unique_inner)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -469,6 +471,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 		ExecInitExpr((Expr *) node->join.plan.qual,
 					 (PlanState *) hjstate);
 	hjstate->js.jointype = node->join.jointype;
+	hjstate->js.unique_inner = node->join.unique_inner;
 	hjstate->js.joinqual = (List *)
 		ExecInitExpr((Expr *) node->join.joinqual,
 					 (PlanState *) hjstate);
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 15742c5..5ee0970 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,12 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semijoin or if the planner found that no more than
+					 * 1 inner row will match a single outer row, we'll
+					 * consider returning the first match, but after that we're
+					 * done with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI || node->js.unique_inner)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1511,6 +1513,7 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 		ExecInitExpr((Expr *) node->join.plan.qual,
 					 (PlanState *) mergestate);
 	mergestate->js.jointype = node->join.jointype;
+	mergestate->js.unique_inner = node->join.unique_inner;
 	mergestate->js.joinqual = (List *)
 		ExecInitExpr((Expr *) node->join.joinqual,
 					 (PlanState *) mergestate);
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e66bcda..a31e1d5 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,11 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * In a semijoin or if the planner found that no more than 1 inner
+			 * row will match a single outer row, we'll consider returning the
+			 * first match, but after that we're done with this outer tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.jointype == JOIN_SEMI || node->js.unique_inner)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -327,6 +328,7 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 		ExecInitExpr((Expr *) node->join.plan.qual,
 					 (PlanState *) nlstate);
 	nlstate->js.jointype = node->join.jointype;
+	nlstate->js.unique_inner = node->join.unique_inner;
 	nlstate->js.joinqual = (List *)
 		ExecInitExpr((Expr *) node->join.joinqual,
 					 (PlanState *) nlstate);
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 9fe8008..d8df18c 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -641,6 +641,7 @@ CopyJoinFields(const Join *from, Join *newnode)
 	CopyPlanFields((const Plan *) from, (Plan *) newnode);
 
 	COPY_SCALAR_FIELD(jointype);
+	COPY_SCALAR_FIELD(unique_inner);
 	COPY_NODE_FIELD(joinqual);
 }
 
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 5a9daf0..3baa902 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2658,13 +2658,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		path->jpath.unique_inner)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * SEMI, ANTI joins and joins that the planner found to have a unique
+		 * inner side will stop after first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index eb65c97..f3cf3cb 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -49,7 +49,7 @@ static List *generate_join_implied_equalities_broken(PlannerInfo *root,
 										Relids outer_relids,
 										Relids nominal_inner_relids,
 										RelOptInfo *inner_rel);
-static Oid select_equality_operator(EquivalenceClass *ec,
+Oid select_equality_operator(EquivalenceClass *ec,
 						 Oid lefttype, Oid righttype);
 static RestrictInfo *create_join_clause(PlannerInfo *root,
 				   EquivalenceClass *ec, Oid opno,
@@ -1282,7 +1282,7 @@ generate_join_implied_equalities_broken(PlannerInfo *root,
  *
  * Returns InvalidOid if no operator can be found for this datatype combination
  */
-static Oid
+Oid
 select_equality_operator(EquivalenceClass *ec, Oid lefttype, Oid righttype)
 {
 	ListCell   *lc;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 11d3933..3062d8c 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -32,12 +32,47 @@
 #include "utils/lsyscache.h"
 
 /* local functions */
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo, RelOptInfo **uniquerel);
+static bool eclassjoin_is_unique_join(PlannerInfo *root, List *joinlist,
+					  RangeTblRef *idxrelrtr);
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, joinlist)
+	{
+		RangeTblRef		*rtr = (RangeTblRef *) lfirst(lc);
+		bool			 uniquejoin;
+
+		if (!IsA(rtr, RangeTblRef))
+			continue;
+
+		uniquejoin = eclassjoin_is_unique_join(root, joinlist, rtr);
+
+		root->simple_rel_array[rtr->rtindex]->is_unique_join = uniquejoin;
+	}
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+		RelOptInfo		*uniquerel;
+
+		/*
+		 * Determine if the inner side of the join can produce at most 1 row
+		 * for any single outer row.
+		 */
+		if (specialjoin_is_unique_join(root, sjinfo, &uniquerel))
+			uniquerel->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -91,6 +126,16 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * The removal of the join could have caused push down quals to
+		 * have been removed, which could previously have stopped us
+		 * from marking the join as a unique join. We'd better make
+		 * another pass over each join to make sure otherwise we may fail
+		 * to remove other joins which had a join condition on the join
+		 * that we just removed.
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -151,18 +196,12 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
-	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
-	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
-		sjinfo->delay_upper_joins)
+	/* We can only remove LEFT JOINs */
+	if (sjinfo->jointype != JOIN_LEFT)
 		return false;
 
 	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
@@ -170,38 +209,11 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
+	/* only joins that don't duplicate their outer side can be removed */
+	if (!innerrel->is_unique_join)
 		return false;
 
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -253,6 +265,281 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+static bool
+eclassjoin_is_unique_join(PlannerInfo *root, List *joinlist, RangeTblRef *idxrelrtr)
+{
+	ListCell   *lc;
+	Query	   *subquery = NULL;
+	RelOptInfo *idxrel;
+
+	idxrel = find_base_rel(root, idxrelrtr->rtindex);
+
+	/* check if we've already marked this join as unique on a previous call */
+	if (idxrel->is_unique_join)
+		return true;
+
+	/*
+	 * The only relation type that can help us is a base rel. If the relation
+	 * does not have any eclass joins then we'll assume that it's not an
+	 * equality type join, therefore won't be provably unique.
+	 */
+	if (idxrel->reloptkind != RELOPT_BASEREL ||
+		idxrel->has_eclass_joins == false)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the rel.
+	 */
+	if (idxrel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation rel, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's no point in going
+		 * further.
+		 */
+		if (idxrel->indexlist == NIL)
+			return false;
+	}
+	else if (idxrel->rtekind == RTE_SUBQUERY)
+	{
+		subquery = root->simple_rte_array[idxrelrtr->rtindex]->subquery;
+
+		/*
+		 * If the subquery has no qualities that support distinctness proofs
+		 * then there's no point in going further.
+		 */
+		if (!query_supports_distinctness(subquery))
+			return false;
+	}
+	else
+		return false;			/* unsupported rtekind */
+
+	/*
+	 * We now search through each relation in the join list searching for every
+	 * relation which joins to idxrel. Our goal here is to deteremine if idxrel
+	 * will match at most 1 row for each of the relations it joins to. In order
+	 * to prove this we'll analyze unique indexes for plain relations, or if
+	 * it's a subquery we'll look at GROUP BY and DISTINCT clauses.
+	 */
+
+	foreach (lc, joinlist)
+	{
+		RangeTblRef	   *rtr = (RangeTblRef *) lfirst(lc);
+		RelOptInfo	   *rel;
+		ListCell	   *lc2;
+		List		   *index_exprs;
+		List		   *operator_list;
+		bool			foundjoin = false;
+
+		/* Skip ourself, and anything that's not a RangeTblRef */
+		if (rtr == idxrelrtr || !IsA(rtr, RangeTblRef))
+			continue;
+
+		rel = find_base_rel(root, rtr->rtindex);
+
+		/* No point in going any further if this rel has no eclass joins */
+		if (rel->has_eclass_joins == false)
+			continue;
+
+		index_exprs = NIL;
+		operator_list = NIL;
+
+		/* now populate the lists with the join condition Vars from rel */
+		foreach(lc2, root->eq_classes)
+		{
+			EquivalenceClass *ec = (EquivalenceClass *) lfirst(lc2);
+
+			/*
+			 * Any eclasses that don't have at least 2 members or have
+			 * volatile expressions are useless to us. Skip these.
+			 */
+			if (list_length(ec->ec_members) < 2 ||
+				ec->ec_has_volatile)
+				continue;
+
+			/* Does this eclass have members for both these relations? */
+			if (bms_overlap(idxrel->relids, ec->ec_relids) &&
+				bms_overlap(rel->relids, ec->ec_relids))
+			{
+				ListCell *lc3;
+				EquivalenceMember *relecm = NULL;
+				EquivalenceMember *idxrelecm = NULL;
+
+				foundjoin = true;
+
+				foreach (lc3, ec->ec_members)
+				{
+					EquivalenceMember	*ecm = (EquivalenceMember *) lfirst(lc3);
+					int					 ecmrel;
+
+					/*
+					 * We're only interested in ec members for a single rel.
+					 * Expressions such as t1.col + t2.col or function calls
+					 * where the parameters come from more than 1 rel are out.
+					 */
+					if (!bms_get_singleton_member(ecm->em_relids, &ecmrel))
+						continue;
+
+					if (ecmrel == rel->relid)
+					{
+						if (relecm != NULL)
+							return false;
+						relecm = ecm;
+					}
+
+					else if (ecmrel == idxrel->relid)
+					{
+						if (idxrelecm != NULL)
+							return false;
+						idxrelecm = ecm;
+					}
+				}
+
+				/*
+				 * We did make a quick check above to ensure that this eclass
+				 * contained members for both relations, but we could have
+				 * found some join condition expression that referenced both
+				 * relations, where we need an singleton type expression is
+				 * what we need.
+				 */
+				if (relecm != NULL && idxrelecm != NULL)
+				{
+					Oid opno = select_equality_operator(ec,
+								relecm->em_datatype, idxrelecm->em_datatype);
+
+					if (!OidIsValid(opno))
+						return false;
+
+					index_exprs = lappend(index_exprs, idxrelecm->em_expr);
+					operator_list = lappend_oid(operator_list, opno);
+				}
+			}
+		}
+
+		/*
+		 * If we found no suitable eclass members then these 2 relations either
+		 * have no join condition between them or we found a join condition but
+		 * there was something about it that's not suitable for testing for
+		 * unique properties, in the former case we're safe to skip to the next
+		 * relation in the latter case we'd better bail.
+		 */
+		if (index_exprs == NIL)
+		{
+			if (foundjoin)
+				return false;
+
+			continue;
+		}
+
+		if (idxrel->rtekind == RTE_RELATION)
+		{
+			if (!relation_has_unique_index_for(root, idxrel, NIL, index_exprs, operator_list))
+				return false;
+		}
+		else /* RTE_SUBQUERY */
+		{
+			List	   *colnos = NIL;
+			ListCell   *l;
+
+			/*
+			 * Build the argument lists for query_is_distinct_for: a list of
+			 * output column numbers that the query needs to be distinct over.
+			 */
+			foreach(l, index_exprs)
+			{
+				Var		   *var = (Var *) lfirst(l);
+
+				if (!IsA(var, Var))
+					return false; /* punt if we have a non-Var */
+
+				colnos = lappend_int(colnos, var->varattno);
+			}
+
+			if (!query_is_distinct_for(subquery, colnos, operator_list))
+				return false;
+
+		}
+	}
+
+	return true;
+}
+
+/*
+ * Returns True if it can be proved that this special join will never produce
+ * more than 1 row per outer row, otherwise returns false if there is
+ * insufficient evidence to prove the join is unique. uniquerel is always
+ * set to the RelOptInfo of the rel that was proved to be unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo,
+						   RelOptInfo **uniquerel)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Query	   *subquery = NULL;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's any delayed upper joins then we'll need to punt. */
+	if (sjinfo->delay_upper_joins)
+		return false;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	/* check if we've already marked this join as unique on a previous call */
+	if (innerrel->is_unique_join)
+	{
+		*uniquerel = innerrel;
+		return true;
+	}
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (innerrel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation innerrel, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's no point in going
+		 * further.
+		 */
+		if (innerrel->indexlist == NIL)
+			return false;
+	}
+	else if (innerrel->rtekind == RTE_SUBQUERY)
+	{
+		subquery = root->simple_rte_array[innerrelid]->subquery;
+
+		/*
+		 * If the subquery has no qualities that support distinctness proofs
+		 * then there's no point in going further.
+		 */
+		if (!query_supports_distinctness(subquery))
+			return false;
+	}
+	else
+		return false;			/* unsupported rtekind */
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -310,7 +597,10 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	{
 		/* Now examine the indexes to see if we have a matching unique index */
 		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
+		{
+			*uniquerel = innerrel;
 			return true;
+		}
 	}
 	else	/* innerrel->rtekind == RTE_SUBQUERY */
 	{
@@ -358,7 +648,10 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		}
 
 		if (query_is_distinct_for(subquery, colnos, opids))
+		{
+			*uniquerel = innerrel;
 			return true;
+		}
 	}
 
 	/*
@@ -368,7 +661,6 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	return false;
 }
 
-
 /*
  * Remove the target relid from the planner's data structures, having
  * determined that there is no need to include it in the query.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index cb69c03..94bf23f 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -132,12 +132,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinType jointype, bool unique_inner);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinType jointype, bool unique_inner);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -152,7 +152,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinType jointype, bool unique_inner);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2192,7 +2192,8 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path->jointype,
+							  best_path->unique_inner);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->path);
 
@@ -2486,7 +2487,8 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   best_path->jpath.jointype,
+							   best_path->jpath.unique_inner);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
@@ -2612,7 +2614,8 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  best_path->jpath.jointype,
+							  best_path->jpath.unique_inner);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3717,7 +3720,8 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinType jointype,
+			  bool unique_inner)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3728,6 +3732,7 @@ make_nestloop(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->join.jointype = jointype;
+	node->join.unique_inner = unique_inner;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
 
@@ -3741,7 +3746,8 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinType jointype,
+			  bool unique_inner)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3753,6 +3759,7 @@ make_hashjoin(List *tlist,
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
 	node->join.jointype = jointype;
+	node->join.unique_inner = unique_inner;
 	node->join.joinqual = joinclauses;
 
 	return node;
@@ -3801,7 +3808,8 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinType jointype,
+			   bool unique_inner)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3817,6 +3825,7 @@ make_mergejoin(List *tlist,
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
 	node->join.jointype = jointype;
+	node->join.unique_inner = unique_inner;
 	node->join.joinqual = joinclauses;
 
 	return node;
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 848df97..9fcd047 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -175,6 +175,12 @@ query_planner(PlannerInfo *root, List *tlist,
 	fix_placeholder_input_needed_levels(root);
 
 	/*
+	 * Analyze all joins and mark which ones can produce at most 1 row
+	 * for each outer row.
+	 */
+	mark_unique_joins(root, joinlist);
+
+	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
 	 * until we've built baserel data structures and classified qual clauses.
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 1395a21..b5ac730 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1722,6 +1722,7 @@ create_nestloop_path(PlannerInfo *root,
 								  &restrict_clauses);
 	pathnode->path.pathkeys = pathkeys;
 	pathnode->jointype = jointype;
+	pathnode->unique_inner = inner_path->parent->is_unique_join;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
@@ -1779,6 +1780,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  &restrict_clauses);
 	pathnode->jpath.path.pathkeys = pathkeys;
 	pathnode->jpath.jointype = jointype;
+	pathnode->jpath.unique_inner = inner_path->parent->is_unique_join;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
@@ -1847,6 +1849,7 @@ create_hashjoin_path(PlannerInfo *root,
 	 */
 	pathnode->jpath.path.pathkeys = NIL;
 	pathnode->jpath.jointype = jointype;
+	pathnode->jpath.unique_inner = inner_path->parent->is_unique_join;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 59b17f3..805653d 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1561,6 +1561,7 @@ typedef struct JoinState
 {
 	PlanState	ps;
 	JoinType	jointype;
+	bool		unique_inner;	/* Inner side will match 0 or 1 outer rows */
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
 } JoinState;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index f6683f0..0b40f41 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -525,9 +525,12 @@ typedef struct CustomScan
 /* ----------------
  *		Join node
  *
- * jointype:	rule for joining tuples from left and right subtrees
- * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
- *				(plan.qual contains conditions that came from WHERE)
+ * jointype:		rule for joining tuples from left and right subtrees
+ * unique_inner:	true if the planner discovered that each inner tuple
+ *					can only match at most 1 outer tuple. Proofs include
+ *					UNIQUE indexes and GROUP BY/DISTINCT clauses etc.
+ * joinqual:		qual conditions that came from JOIN/ON or JOIN/USING
+ *					(plan.qual contains conditions that came from WHERE)
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -542,6 +545,7 @@ typedef struct Join
 {
 	Plan		plan;
 	JoinType	jointype;
+	bool		unique_inner;	/* Inner side will match 0 or 1 outer rows */
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
 } Join;
 
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 6845a40..509758e 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -470,6 +470,8 @@ typedef struct RelOptInfo
 	List	   *joininfo;		/* RestrictInfo structures for join clauses
 								 * involving this rel */
 	bool		has_eclass_joins;		/* T means joininfo is incomplete */
+	bool		is_unique_join;		/* True if this rel won't contain more
+									 * than 1 row for the join condition */
 } RelOptInfo;
 
 /*
@@ -1028,6 +1030,8 @@ typedef struct JoinPath
 
 	JoinType	jointype;
 
+	bool		unique_inner;	/* inner side of join is unique */
+
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
 
@@ -1404,6 +1408,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		unique_inner;	/* inner side is unique on join condition */
 	List	   *join_quals;		/* join quals, in implicit-AND list format */
 } SpecialJoinInfo;
 
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 6cad92e..74f3a1f 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -119,6 +119,8 @@ extern List *generate_join_implied_equalities(PlannerInfo *root,
 								 Relids join_relids,
 								 Relids outer_relids,
 								 RelOptInfo *inner_rel);
+extern Oid select_equality_operator(EquivalenceClass *ec, Oid lefttype,
+								 Oid righttype);
 extern bool exprs_known_equal(PlannerInfo *root, Node *item1, Node *item2);
 extern void add_child_rel_equivalences(PlannerInfo *root,
 						   AppendRelInfo *appinfo,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index fa72918..2cddad8 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -122,6 +122,7 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index ca3a17b..0f2d031 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3111,6 +3111,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Unique Join: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3118,12 +3119,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Unique Join: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3210,7 +3212,7 @@ select id from a where id in (
 );
          QUERY PLAN         
 ----------------------------
- Hash Semi Join
+ Hash Join
    Hash Cond: (a.id = b.id)
    ->  Seq Scan on a
    ->  Hash
@@ -3926,12 +3928,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -3958,12 +3961,13 @@ select * from
 ----------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, 42::bigint))
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, 42::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -3991,6 +3995,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Unique Join: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -3998,7 +4003,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4018,12 +4023,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Unique Join: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4045,10 +4051,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Unique Join: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Unique Join: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4057,7 +4065,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4103,10 +4111,12 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Unique Join: No
          Join Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
@@ -4114,7 +4124,7 @@ select * from
                Output: c.q1
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1
-(13 rows)
+(15 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -4181,13 +4191,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, 42::bigint)), d.q1, (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, 42::bigint)), d.q2)))
+   Unique Join: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, 42::bigint)), (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2))
+         Unique Join: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, 42::bigint)), (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2))
+               Unique Join: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, 42::bigint))
+                     Unique Join: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -4203,7 +4217,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -4221,17 +4235,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Unique Join: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Unique Join: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Unique Join: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Unique Join: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Unique Join: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -4253,7 +4272,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -4266,16 +4285,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Unique Join: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Unique Join: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- test some error cases where LATERAL should have been used but wasn't
 select f1,g from int4_tbl a, (select f1 as g) ss;
@@ -4358,3 +4379,261 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Unique Join: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Join: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Unique Join: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Join: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Join: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Join: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Unique Join: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure distinct clause uniquifies the join 
+explain (verbose, costs off)
+select * from j1 
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Join: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  HashAggregate
+         Output: j3.id
+         Group Key: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(11 rows)
+
+-- ensure group by clause uniquifies the join 
+explain (verbose, costs off)
+select * from j1 
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Join: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  HashAggregate
+         Output: j3.id
+         Group Key: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(11 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Unique Join: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Join: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Join: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure no unique joins when the tables are joined in 3 ways.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id2 and j1.id2 = j2.id2
+inner join j3 on j1.id1 = j3.id1 and j2.id2 = j3.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2, j3.id1, j3.id2
+   Unique Join: No
+   Join Filter: (j1.id1 = j3.id1)
+   ->  Nested Loop
+         Output: j1.id1, j1.id2, j2.id1, j2.id2
+         Unique Join: No
+         Join Filter: (j1.id1 = j2.id2)
+         ->  Seq Scan on public.j1
+               Output: j1.id1, j1.id2
+               Filter: (j1.id1 = j1.id2)
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j3
+         Output: j3.id1, j3.id2
+         Filter: (j3.id1 = j3.id2)
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 7991e99..99feee4 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2016,12 +2016,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2042,11 +2043,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index b14410f..7f43784 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -761,6 +761,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Unique Join: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -769,7 +770,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -789,6 +790,7 @@ select * from int4_tbl o where (f1, f1) in
 ----------------------------------------------------------------------
  Hash Join
    Output: o.f1
+   Unique Join: No
    Hash Cond: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -805,7 +807,7 @@ select * from int4_tbl o where (f1, f1) in
                            Group Key: i.f1
                            ->  Seq Scan on public.int4_tbl i
                                  Output: i.f1
-(18 rows)
+(19 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index c49e769..b481cda 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2071,6 +2071,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 3) AND leakproof(t1_5.a))
@@ -2088,6 +2089,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t11
                            Output: t11.ctid, t11.a, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 3) AND leakproof(t11.a))
@@ -2105,6 +2107,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 3) AND leakproof(t12_2.a))
@@ -2122,6 +2125,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 3) AND leakproof(t111_3.a))
@@ -2132,7 +2136,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 3)
-(69 rows)
+(73 rows)
 
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
 SELECT * FROM v1 WHERE a=100; -- Nothing should have been changed to 100
@@ -2157,6 +2161,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 8) AND leakproof(t1_5.a))
@@ -2174,6 +2179,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t11
                            Output: t11.a, t11.ctid, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 8) AND leakproof(t11.a))
@@ -2191,6 +2197,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 8) AND leakproof(t12_2.a))
@@ -2208,6 +2215,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 8) AND leakproof(t111_3.a))
@@ -2218,7 +2226,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 8)
-(69 rows)
+(73 rows)
 
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
 NOTICE:  snooped value: 8
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 524e0ef..1eb2da4 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2093,6 +2093,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: 42::bigint, 47::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Unique Join: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2100,6 +2101,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Unique Join: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2107,6 +2109,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Unique Join: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2114,12 +2117,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Unique Join: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(34 rows)
+(38 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 6005476..408cf20 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1276,3 +1276,99 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+-- ensure no unique joins when the tables are joined in 3 ways.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id2 and j1.id2 = j2.id2
+inner join j3 on j1.id1 = j3.id1 and j2.id2 = j3.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#12David Rowley
dgrowleyml@gmail.com
In reply to: Tomas Vondra (#5)
Re: Performance improvement for joins where outer side is unique

On 26 February 2015 at 13:15, Tomas Vondra <tomas.vondra@2ndquadrant.com>
wrote:

BTW, I find this coding (first cast, then check) rather strange:

Var *var = (Var *) ecm->em_expr;

if (!IsA(var, Var))
continue; /* Ignore Consts */

It's probably harmless, but I find it confusing and I can't remember
seeing it elsewhere in the code (for example clausesel.c and such) use
this style:

... clause is (Node*) ...

if (IsA(clause, Var))
{
Var *var = (Var*)clause;
...
}

or

Var * var = NULL;

if (! IsA(clause, Var))
// error / continue

var = (Var*)clause;

Yeah, it does look a bit weird, but if you search the code for "IsA(var,
Var)" you'll see it's nothing new.

Regards

David Rowley

#13Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: David Rowley (#9)
Re: Performance improvement for joins where outer side is unique

Sorry for the dealy, I've returned to this.

Performance:

I looked on the performance gain this patch gives. For several
on-memory joins, I had gains about 3% for merge join, 5% for hash
join, and 10% for nestloop (@CentOS6), for simple 1-level joins
with aggregation similar to what you mentioned in previous
mail like this.

=# SELECT count(*) FROM t1 JOIN t2 USING (id);

Of course, the gain would be trivial when returning many tuples,
or scans go to disk.

That's true, but joins where rows are filtered after the join condition
will win here too:

Yes, when not many tuples was returned, the gain remains in
inverse proportion to the number of the returning rows. Heavy
(time-consuming) on-memory join returning several rows should
have gain from this.

Also queries with a GROUP BY clause. (Since grouping is always performed
after the join :-( )

And in many cases clinged by additional sorts:p As such, so many
factors affect the performance so improvements which give linear
performance gain are often difficult to gain approval to being
applied. I suppose we need more evidence how and in what
situation we will receive the gain.

I haven't measured the loss by additional computation when the

query is regarded as not a "single join".

I think this would be hard to measure, but likely if it is measurable then
you'd want to look at just planning time, rather than planning and
execution time.

From the another point of view, the patch looks a bit large for
the gain (for me). Addition to it, it loops by many levels.

[mark_unique_joins()]
foreach (joinlist)
[eclassjoin_is_unique_join()]
foreach(joinlist)
foreach(root->eq_classes)
foreach(root->ec_members)

The innermost loop could be roughly said to iterate about 10^3*2
times for a join of 10 tables all of which have index and no
eclass is shared among them. From my expreince, we must have the
same effect by far less iteration levels.

This is caueed the query like this, as a bad case.

create table t1 (a int, b int);
create index i_t1 (a int);
create table t2 (like t1 including indexes);
....
create table t10 (.....);
insert into t1 (select a, a * 10 from generate_series(0, 100) a);
insert into t2 (select a * 10, a * 20 from generate_series(0, 100) a);
...
insert into t10 (select a * 90, a * 100 from generate_series(0, 100) a);

explain select t1.a, t10.b from t1 join t2 on (t1.b = t2.a) join t3 on (t2.b = t3.a) join t4 on (t3.b = t4.a) join t5 on (t4.b = t5.a) join t6 on (t5.b = t6.a) join t7 on (t6.b = t7.a) join t8 on (t7.b = t8.a) join t9 on (t8.b = t9.a) join t10 on (t9.b = t10.a);

The head takes 3ms for planning and the patched version takes
around 5ms while pure execution time is 1ms.. I think it is a too
long extra time.

Explain representation:

I don't like that the 'Unique Join:" occupies their own lines in
the result of explain, moreover, it doesn't show the meaning
clearly. What about the representation like the following? Or,
It might not be necessary to appear there.

Nested Loop ...
Output: ....
-   Unique Jion: Yes
-> Seq Scan on public.t2 (cost = ...
-     -> Seq Scan on public.t1 (cost = ....
+     -> Seq Scan on public.t1 (unique) (cost = ....

Yeah I'm not too big a fan of this either. I did at one evolution of the
patch I had "Unique Left Join" in the join node's line in the explain
output, but I hated that more and changed it just to be just in the VERBOSE
output, and after I did that I didn't want to change the join node's line
only when in verbose mode. I do quite like that it's a separate item for
the XML and JSON explain output. That's perhaps quite useful when the
explain output must be processed by software.

I'm totally open for better ideas on names, but I just don't have any at
the moment.

"Unique Left Join" looks too bold. Anyway changing there is
rather easier.

Coding:

The style looks OK. Could applied on master.
It looks to work as you expected for some cases.

Random comments follow.

- Looking specialjoin_is_unique_join, you seem to assume that
!delay_upper_joins is a sufficient condition for not being
"unique join". The former simplly indicates that "don't
commute with upper OJs" and the latter seems to indicate that
"the RHS is unique", Could you tell me what kind of
relationship is there between them?

The rationalisation around that are from the (now changed) version of
join_is_removable(), where the code read:

/*
* Must be a non-delaying left join to a single baserel, else we aren't
* going to be able to do anything with it.
*/
if (sjinfo->jointype != JOIN_LEFT ||
sjinfo->delay_upper_joins)
return false;

I have to admit that I didn't go and investigate why delayed upper joins
cannot be removed by left join removal code, I really just assumed that
we're just unable to prove that a join to such a relation won't match more
than one outer side's row. I kept this to maintain that behaviour as I
assumed it was there for a good reason.

Yes, I suppose that you thought so and it should work as expected
as a logical calculation for now. But delay_upper_joins is the
condition for not being allowed to be removed and the meaning
looks not to have been changed, right? If so, I think they should
be treated according to their right meanings.

Specifically, delay_upper_joins should be removed from the
condition for unique_join to the original location, at the very
first of join_is_removable.

- The naming "unique join" seems a bit obscure for me, but I
don't have no better idea:( However, the member name
"has_unique_join" seems to be better to be "is_unique_join".

Yeah, I agree with this. I just can't find something short enough that
means "based on the join condition, the inner side of the join will never
produce more than 1 row for any single outer row". Unique join was the best
I came up with. I'm totally open for better ideas.

It should be good to write the precise definition of "unique
join" in the appropriate comment.

I agree that is_unique_join is better than has_unique_join. I must have
just copied the has_eclass_joins struct member without thinking too hard
about it. I've now changed this in my local copy of the patch.

- eclassjoin_is_unique_join involves seemingly semi-exhaustive

scan on ec members for every element in joinlist. Even if not,
it might be thought to be a bit too large for the gain. Could
you do the equivelant things by more small code?

I'd imagine some very complex queries could have many equivalence classes
and members, though I hadn't really thought that this number would be so
great that processing here would suffer much. There's quite a few fast
paths out, like the test to ensure that both rels are mentioned
in ec_relids. Though for a query which is so complex to have a great number
of eclass' and members, likely there would be quite a few relations
involved and planning would be slower anyway. I'm not quite sure how else I
could write this and still find the unique join cases each time. We can't
really just give up half way through looking.

A rough estimate for a bad case is mentioned above. I had the
similar comment about the complexity for code to find implicitly
uniquely ordered join peer. Perhaps there's many improvable
points implemented easily by looping over join_list and
eclasses. But we would shuold do them without piled loops.

regards,

--
Kyotaro Horiguchi
NTT Open Source Software Center

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

#14David Rowley
dgrowleyml@gmail.com
In reply to: Kyotaro HORIGUCHI (#13)
2 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 10 March 2015 at 19:19, Kyotaro HORIGUCHI <
horiguchi.kyotaro@lab.ntt.co.jp> wrote:

From the another point of view, the patch looks a bit large for
the gain (for me). Addition to it, it loops by many levels.

[mark_unique_joins()]
foreach (joinlist)
[eclassjoin_is_unique_join()]
foreach(joinlist)
foreach(root->eq_classes)
foreach(root->ec_members)

The innermost loop could be roughly said to iterate about 10^3*2
times for a join of 10 tables all of which have index and no
eclass is shared among them. From my expreince, we must have the
same effect by far less iteration levels.

This is caueed the query like this, as a bad case.

create table t1 (a int, b int);
create index i_t1 (a int);
create table t2 (like t1 including indexes);
....
create table t10 (.....);
insert into t1 (select a, a * 10 from generate_series(0, 100) a);
insert into t2 (select a * 10, a * 20 from generate_series(0, 100) a);
...
insert into t10 (select a * 90, a * 100 from generate_series(0, 100) a);

explain select t1.a, t10.b from t1 join t2 on (t1.b = t2.a) join t3 on
(t2.b = t3.a) join t4 on (t3.b = t4.a) join t5 on (t4.b = t5.a) join t6 on
(t5.b = t6.a) join t7 on (t6.b = t7.a) join t8 on (t7.b = t8.a) join t9 on
(t8.b = t9.a) join t10 on (t9.b = t10.a);

The head takes 3ms for planning and the patched version takes
around 5ms while pure execution time is 1ms.. I think it is a too
long extra time.

This smells quite fishy to me. I'd be quite surprised if your machine took
an extra 2 ms to do this.

I've run what I think is the same test on my 5 year old i5 laptop and
attached the .sql file which I used to generate the same schema as you've
described.

I've also attached the results of the explain analyze "Planning Time:"
output from patched and unpatched using your test case.

I was unable to notice any difference in plan times between both versions.
In fact master came out slower, which is likely just the noise in the
results.

Just to see how long mark_unique_joins() takes with your test case I
changed query_planner() to call mark_unique_joins() 1 million times:

{
int x;
for (x = 0; x < 1000000;x++)
mark_unique_joins(root, joinlist);
}

I also got rid of the fast path test which bails out if the join is already
marked as unique.

/* check if we've already marked this join as unique on a previous call */
/*if (idxrel->is_unique_join)
return true;
*/

On my machine after making these changes, it takes 800 ms to plan the
query. So it seems that's around 800 nano seconds for a single call to
mark_unique_joins().

Perhaps you've accidentally compiled the patched version with debug asserts?

Are you able to retest this?

Regards

David Rowley

Attachments:

unijoin_plan_benchmark.sqltext/plain; charset=US-ASCII; name=unijoin_plan_benchmark.sqlDownload
unijoin_plan_benchmark_results.xlsxapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet; name=unijoin_plan_benchmark_results.xlsxDownload
#15Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: David Rowley (#14)
Re: Performance improvement for joins where outer side is unique

Hello. The performance drag was not so large after all.

For all that, I agree that the opition that this kind of separate
multiple-nested loops on relations, joins or ECs and so on for
searching something should be avoided. I personally feel that
additional time to such an extent (around 1%) would be tolerable
if it affected a wide range of queries or it brought more obvious
gain.

Unfortunately I can't decide this to be 'ready for commiter' for
now. I think we should have this on smaller footprint, in a
method without separate exhauxtive searching. I also had very
similar problem in the past but I haven't find such a way for my
problem..

At Wed, 11 Mar 2015 01:32:24 +1300, David Rowley <dgrowleyml@gmail.com> wrote in <CAApHDvpEXAjs6mV2ro4=3qbzpx=pLrteinX0J2YHq6wrp85pPw@mail.gmail.com>

explain select t1.a, t10.b from t1 join t2 on (t1.b = t2.a) join t3 on
(t2.b = t3.a) join t4 on (t3.b = t4.a) join t5 on (t4.b = t5.a) join t6 on
(t5.b = t6.a) join t7 on (t6.b = t7.a) join t8 on (t7.b = t8.a) join t9 on
(t8.b = t9.a) join t10 on (t9.b = t10.a);

The head takes 3ms for planning and the patched version takes
around 5ms while pure execution time is 1ms.. I think it is a too
long extra time.

This smells quite fishy to me. I'd be quite surprised if your machine took
an extra 2 ms to do this.

You're right. Sorry. I was amazed by the numbers..

I took again the times for both master and patched on master
(some conflict arised). Configured with no options so compiled
with -O2 and no assertions. Measured the planning time for the
test query 10 times and calcualted the average.

patched: 1.883ms (stddev 0.034)
master: 1.861ms (stddev 0.042)

About 0.02ms, 1% extra time looks to be taken by the extra
processing.

regards,

I've run what I think is the same test on my 5 year old i5 laptop and
attached the .sql file which I used to generate the same schema as you've
described.

I've also attached the results of the explain analyze "Planning Time:"
output from patched and unpatched using your test case.

I was unable to notice any difference in plan times between both versions.
In fact master came out slower, which is likely just the noise in the
results.

Just to see how long mark_unique_joins() takes with your test case I
changed query_planner() to call mark_unique_joins() 1 million times:

{
int x;
for (x = 0; x < 1000000;x++)
mark_unique_joins(root, joinlist);
}

I also got rid of the fast path test which bails out if the join is already
marked as unique.

/* check if we've already marked this join as unique on a previous call */
/*if (idxrel->is_unique_join)
return true;
*/

On my machine after making these changes, it takes 800 ms to plan the
query. So it seems that's around 800 nano seconds for a single call to
mark_unique_joins().

Perhaps you've accidentally compiled the patched version with debug asserts?

Are you able to retest this?

--
Kyotaro Horiguchi
NTT Open Source Software Center

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

#16David Rowley
dgrowleyml@gmail.com
In reply to: Kyotaro HORIGUCHI (#15)
Re: Performance improvement for joins where outer side is unique

On 13 March 2015 at 20:34, Kyotaro HORIGUCHI <
horiguchi.kyotaro@lab.ntt.co.jp> wrote:

Hello. The performance drag was not so large after all.

Great, thanks for retesting this.

For all that, I agree that the opition that this kind of separate
multiple-nested loops on relations, joins or ECs and so on for
searching something should be avoided. I personally feel that
additional time to such an extent (around 1%) would be tolerable
if it affected a wide range of queries or it brought more obvious
gain.

Do you have any ideas on an implementation of how we can avoid checking all
eclasses for each combination of joins?

The only thing I can think of would be to add a List of eclasses on
RelOptInfo for all eclasses that the RelOptInfo has. That would save the
looping over every single eclass in eclassjoin_is_unique_join() and save
from having to check if the relation is mentioned in the eclass before
continuing. I could certainly make this change, but I'd be worried that it
would just add bloat to the patch and cause a committed to question it.

I could also almost completely remove the extra plan time of your test case
by either adding a proper pre-check to ensure the relation has unique
indexes, rather than just indexes or I could add a new list to RelOptInfo
which stores unique index, then I could just check if that's NIL before
going any further in eclassjoin_is_unique_join(), but I come from a world
where every relation has a primary key, so I'd just imagine that would not
be hit in enough real cases. I'd imagine that pre-check is only there
because it's so cheap in the first place.

For testing, I added some code to mark_unique_joins() to spit out a NOTICE:

if (eclassjoin_is_unique_join(root, joinlist, rtr))
{
root->simple_rel_array[rtr->rtindex]->is_unique_join = true;
elog(NOTICE, "Unique Join: Yes");
}
else
elog(NOTICE, "Unique Join: No");

and the same below for special joins too.

On running the regression tests I see:

"Unique Join: Yes" 1557 times
"Unique Join: No" 11563 times

I would imagine the regression tests are not the best thing to base this
one as they tend to exercise more unusual cases.
I have an OLTP type application here which I will give a bit of exercise
and see how that compares.

Unfortunately I can't decide this to be 'ready for commiter' for
now. I think we should have this on smaller footprint, in a
method without separate exhauxtive searching. I also had very
similar problem in the past but I haven't find such a way for my
problem..

I don't think it's ready yet either. I've just been going over a few things
and looking at Tom's recent commit b557226 in pathnode.c I've got a feeling
that this patch would need to re-factor some code that's been modified
around the usage of relation_has_unique_index_for() as when this code is
called, the semi joins have already been analysed to see if they're unique,
so it could be just a case of ripping all of that out
of create_unique_path() and just putting a check to say rel->is_unique_join
in there. But if I do that then I'm not quite sure if
SpecialJoinInfo->semi_rhs_exprs and SpecialJoinInfo->semi_operators would
still be needed at all. These were only added in b557226. Changing this
would help reduce the extra planning time when the query contains
semi-joins. To be quite honest, this type of analysis belongs in
analyzejoin.c anyway. I'm tempted to hack at this part some more, but I'd
rather Tom had a quick glance at what I'm trying to do here first.

At Wed, 11 Mar 2015 01:32:24 +1300, David Rowley <dgrowleyml@gmail.com>
wrote in <CAApHDvpEXAjs6mV2ro4=3qbzpx=
pLrteinX0J2YHq6wrp85pPw@mail.gmail.com>

explain select t1.a, t10.b from t1 join t2 on (t1.b = t2.a) join t3 on
(t2.b = t3.a) join t4 on (t3.b = t4.a) join t5 on (t4.b = t5.a) join

t6 on

(t5.b = t6.a) join t7 on (t6.b = t7.a) join t8 on (t7.b = t8.a) join

t9 on

(t8.b = t9.a) join t10 on (t9.b = t10.a);

The head takes 3ms for planning and the patched version takes
around 5ms while pure execution time is 1ms.. I think it is a too
long extra time.

This smells quite fishy to me. I'd be quite surprised if your machine

took

an extra 2 ms to do this.

You're right. Sorry. I was amazed by the numbers..

I took again the times for both master and patched on master
(some conflict arised). Configured with no options so compiled
with -O2 and no assertions. Measured the planning time for the
test query 10 times and calcualted the average.

patched: 1.883ms (stddev 0.034)
master: 1.861ms (stddev 0.042)

About 0.02ms, 1% extra time looks to be taken by the extra
processing.

Is this still using the same test query as I attached in my previous email?

This is still 25 times more of a slowdown as to what I witnessed by calling
mark_unique_joins() 1 million times in a tight loop, but of course much
closer to what I would have thought. You're getting 20,000 nanoseconds and
I'm getting 800 nanoseconds, but our overall planning times are very
similar, so I assume our processors are of similar speeds.

It's certainly difficult to know if the extra planning will pay off enough
for it to reduce total CPU time between both planning and executing the
query. There actually are cases where the planning time will be reduced by
this patch. In the case when a LEFT JOIN is removed the master code
currently goes off and re-checks all relations to see if the removal of
that 1 relation has caused it to be possible for other relations to be
removed, and this currently involves analysing unique indexes again to see
if the join is still unique. With this patch I've cut out most of the work
which is done in join_is_removable(), it no longer does that unique
analysis of the join condition as that's done in mark_unique_joins(). The
cases that should be faster are, if a join is already marked as unique then
the code will never test that again.

I have quite a few other ideas lined up which depend on knowing if a join
is unique, all these ideas naturally depend on this patch. Really the
5%-10% join speed-up was the best excuse that I could find have this work
actually accepted. My first email in this thread explains some of my other
ideas.

In addition to those ideas I've been thinking about how that if we could
determine that a plan returns a single row, e.g a key lookup then we could
likely safely cache those plans, as there were never be any data skew in
these situations. I have no immediate plans to go and work on this, but
quite possibly I will in the future if I manage to get unique joins in
first.

Regards

David Rowley

#17David Rowley
dgrowleyml@gmail.com
In reply to: David Rowley (#16)
Re: Performance improvement for joins where outer side is unique

On 14 March 2015 at 14:51, David Rowley <dgrowleyml@gmail.com> wrote:

On 13 March 2015 at 20:34, Kyotaro HORIGUCHI <
horiguchi.kyotaro@lab.ntt.co.jp> wrote:

For all that, I agree that the opition that this kind of separate

multiple-nested loops on relations, joins or ECs and so on for
searching something should be avoided. I personally feel that
additional time to such an extent (around 1%) would be tolerable
if it affected a wide range of queries or it brought more obvious
gain.

For testing, I added some code to mark_unique_joins() to spit out a NOTICE:

if (eclassjoin_is_unique_join(root, joinlist, rtr))
{
root->simple_rel_array[rtr->rtindex]->is_unique_join = true;
elog(NOTICE, "Unique Join: Yes");
}
else
elog(NOTICE, "Unique Join: No");

and the same below for special joins too.

On running the regression tests I see:

"Unique Join: Yes" 1557 times
"Unique Join: No" 11563 times

With this notice emitting code in place, I opened up pgAdmin and had a
click around for a few minutes.

If I search the log file I see:

Unique Join: No 940 times
Unique Join: Yes 585 times

It seems that joins with a unique inner side are quite common here.

Regards

David Rowley

#18David Rowley
dgrowleyml@gmail.com
In reply to: David Rowley (#16)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 14 March 2015 at 14:51, David Rowley <dgrowleyml@gmail.com> wrote:

On 13 March 2015 at 20:34, Kyotaro HORIGUCHI <
horiguchi.kyotaro@lab.ntt.co.jp> wrote:

Unfortunately I can't decide this to be 'ready for commiter' for

now. I think we should have this on smaller footprint, in a

method without separate exhauxtive searching. I also had very
similar problem in the past but I haven't find such a way for my
problem..

I don't think it's ready yet either. I've just been going over a few
things and looking at Tom's recent commit b557226 in pathnode.c I've got a
feeling that this patch would need to re-factor some code that's been
modified around the usage of relation_has_unique_index_for() as when this
code is called, the semi joins have already been analysed to see if they're
unique, so it could be just a case of ripping all of that out
of create_unique_path() and just putting a check to say rel->is_unique_join
in there. But if I do that then I'm not quite sure if
SpecialJoinInfo->semi_rhs_exprs and SpecialJoinInfo->semi_operators would
still be needed at all. These were only added in b557226. Changing this
would help reduce the extra planning time when the query contains
semi-joins. To be quite honest, this type of analysis belongs in
analyzejoin.c anyway. I'm tempted to hack at this part some more, but I'd
rather Tom had a quick glance at what I'm trying to do here first.

I decided to hack away any change the code Tom added in b557226. I've
changed it so that create_unique_path() now simply just uses if
(rel->is_unique_join), instead off all the calls to
relation_has_unique_index_for() and query_is_distinct_for(). This vastly
simplifies that code. One small change is that Tom's checks for uniqueness
on semi joins included checks for volatile functions, this check didn't
exist in the original join removal code, so I've left it out. We'll never
match a expression with a volatile function to a unique index as indexes
don't allow volatile function expressions anyway. So as I understand it
this only serves as a fast path out if the join condition has a volatile
function... But I'd assume that check is not all that cheap.

I ended up making query_supports_distinctness() and query_is_distinct_for()
static in analyzejoins.c as they're not used in any other files.
relation_has_unique_index_for() is also now only used in analyzejoins.c,
but I've not moved it into that file yet as I don't want to bloat the
patch. I just added a comment to say it needs moved.

I've also added a small amount of code to query_is_distinct_for() which
allows subqueries such as (select 1 a offset 0) to be marked as unique. I
thought it was a little silly that these were not being detected as unique,
so I fixed it. This has the side effect of allowing left join removals for
queries such as: select t1.* from t1 left join (select 1 a offset 0) a on
t1.id=a.a;

Updated patch attached.

Regards

David Rowley

Attachments:

unijoin_2015-03-14_81bd96a.patchapplication/octet-stream; name=unijoin_2015-03-14_81bd96a.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a951c55..55a9505 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1268,10 +1268,27 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->format == EXPLAIN_FORMAT_TEXT)
 		appendStringInfoChar(es->str, '\n');
 
-	/* target list */
 	if (es->verbose)
+	{
+		/* target list */
 		show_plan_tlist(planstate, ancestors, es);
 
+		/* unique join */
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *)plan)->unique_inner ? "Yes" : "No";
+				ExplainPropertyText("Unique Join", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 1d78cdf..28fc618 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semijoin or if the planner found that no more than
+					 * 1 inner row will match a single outer row, we'll
+					 * consider returning the first match, but after that we're
+					 * done with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI || node->js.unique_inner)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -469,6 +471,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 		ExecInitExpr((Expr *) node->join.plan.qual,
 					 (PlanState *) hjstate);
 	hjstate->js.jointype = node->join.jointype;
+	hjstate->js.unique_inner = node->join.unique_inner;
 	hjstate->js.joinqual = (List *)
 		ExecInitExpr((Expr *) node->join.joinqual,
 					 (PlanState *) hjstate);
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 15742c5..5ee0970 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,12 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semijoin or if the planner found that no more than
+					 * 1 inner row will match a single outer row, we'll
+					 * consider returning the first match, but after that we're
+					 * done with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI || node->js.unique_inner)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1511,6 +1513,7 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 		ExecInitExpr((Expr *) node->join.plan.qual,
 					 (PlanState *) mergestate);
 	mergestate->js.jointype = node->join.jointype;
+	mergestate->js.unique_inner = node->join.unique_inner;
 	mergestate->js.joinqual = (List *)
 		ExecInitExpr((Expr *) node->join.joinqual,
 					 (PlanState *) mergestate);
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e66bcda..a31e1d5 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,11 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * In a semijoin or if the planner found that no more than 1 inner
+			 * row will match a single outer row, we'll consider returning the
+			 * first match, but after that we're done with this outer tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.jointype == JOIN_SEMI || node->js.unique_inner)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -327,6 +328,7 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 		ExecInitExpr((Expr *) node->join.plan.qual,
 					 (PlanState *) nlstate);
 	nlstate->js.jointype = node->join.jointype;
+	nlstate->js.unique_inner = node->join.unique_inner;
 	nlstate->js.joinqual = (List *)
 		ExecInitExpr((Expr *) node->join.joinqual,
 					 (PlanState *) nlstate);
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 291e6a7..9e0e9b6 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -641,6 +641,7 @@ CopyJoinFields(const Join *from, Join *newnode)
 	CopyPlanFields((const Plan *) from, (Plan *) newnode);
 
 	COPY_SCALAR_FIELD(jointype);
+	COPY_SCALAR_FIELD(unique_inner);
 	COPY_NODE_FIELD(joinqual);
 }
 
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 1a0d358..8740296 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1847,13 +1847,15 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of source data */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI ||
+		path->unique_inner)
 	{
 		double		outer_matched_rows = workspace->outer_matched_rows;
 		Selectivity inner_scan_frac = workspace->inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * SEMI, ANTI join and unique joins: executor will stop after first match.
 		 */
 
 		/* Compute number of tuples processed (not number emitted!) */
@@ -2351,7 +2353,7 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * clauses that are to be applied at the join.  (This is pessimistic since
 	 * not all of the quals may get evaluated at each tuple.)
 	 *
-	 * Note: we could adjust for SEMI/ANTI joins skipping some qual
+	 * Note: we could adjust for SEMI/ANTI and unique joins skipping some qual
 	 * evaluations here, but it's probably not worth the trouble.
 	 */
 	startup_cost += qp_qual_cost.startup;
@@ -2658,13 +2660,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		path->jpath.unique_inner)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * SEMI, ANTI joins and joins that the planner found to have a unique
+		 * inner side will stop after first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index eb65c97..f3cf3cb 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -49,7 +49,7 @@ static List *generate_join_implied_equalities_broken(PlannerInfo *root,
 										Relids outer_relids,
 										Relids nominal_inner_relids,
 										RelOptInfo *inner_rel);
-static Oid select_equality_operator(EquivalenceClass *ec,
+Oid select_equality_operator(EquivalenceClass *ec,
 						 Oid lefttype, Oid righttype);
 static RestrictInfo *create_join_clause(PlannerInfo *root,
 				   EquivalenceClass *ec, Oid opno,
@@ -1282,7 +1282,7 @@ generate_join_implied_equalities_broken(PlannerInfo *root,
  *
  * Returns InvalidOid if no operator can be found for this datatype combination
  */
-static Oid
+Oid
 select_equality_operator(EquivalenceClass *ec, Oid lefttype, Oid righttype)
 {
 	ListCell   *lc;
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index 49ab366..d213970 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -2814,6 +2814,8 @@ ec_member_matches_indexcol(PlannerInfo *root, RelOptInfo *rel,
  * The caller need only supply equality conditions arising from joins;
  * this routine automatically adds in any usable baserestrictinfo clauses.
  * (Note that the passed-in restrictlist will be destructively modified!)
+ *
+ * XXX: this function probably belongs in analyzejoin.c
  */
 bool
 relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 11d3933..dfc267a 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -32,12 +32,50 @@
 #include "utils/lsyscache.h"
 
 /* local functions */
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo, RelOptInfo **uniquerel);
+static bool eclassjoin_is_unique_join(PlannerInfo *root, List *joinlist,
+					  RangeTblRef *targetrtr);
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
+static bool query_supports_distinctness(Query *query);
+static bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+/*
+ * mark_unique_joins
+ *		Analyze join quals to determine if at most 1 row can be produced from
+ *		each relation for all relations that the relation joins to. This
+ *		function uses both unique indexes and DISTINCT/GROUP BY clauses in
+ *		sub-queries in order to determine the uniqueness of the relation.
+ */
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, joinlist)
+	{
+		RangeTblRef		*rtr = (RangeTblRef *) lfirst(lc);
+
+		if (!IsA(rtr, RangeTblRef))
+			continue;
+
+		if (eclassjoin_is_unique_join(root, joinlist, rtr))
+			root->simple_rel_array[rtr->rtindex]->is_unique_join = true;
+	}
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+		RelOptInfo		*uniquerel;
+
+		if (specialjoin_is_unique_join(root, sjinfo, &uniquerel))
+			uniquerel->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -91,6 +129,15 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * The removal of the join could have changed a condition that
+		 * previously caused us not to be able to mark the join as unique.
+		 * We'd better make another pass over each join to make sure otherwise
+		 * we may fail to remove other joins which had a join condition on the
+		 * join that we just removed.
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -151,11 +198,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
 	 * Must be a non-delaying left join to a single baserel, else we aren't
@@ -170,38 +215,11 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
+	/* only joins that don't duplicate their outer side can be removed */
+	if (!innerrel->is_unique_join)
 		return false;
 
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -212,7 +230,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -253,6 +272,302 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * eclassjoin_is_unique_join
+ *		Returns True if targetrtr can be proved to never produce more than 1
+ *		row for each single row of the relations which is joins to.
+ *		Proofs of uniqueness include unique indexes and GROUP BY/DISTINCT
+ *		clauses for sub queries.
+ */
+static bool
+eclassjoin_is_unique_join(PlannerInfo *root, List *joinlist, RangeTblRef *targetrtr)
+{
+	ListCell   *lc;
+	Query	   *subquery;
+	RelOptInfo *targetrel;
+
+	/* if there's only 1 relation then there's no joins to prove unique */
+	if (list_length(joinlist) <= 1)
+		return false;
+
+	targetrel = find_base_rel(root, targetrtr->rtindex);
+
+	/* check if we've already marked this join as unique on a previous call */
+	if (targetrel->is_unique_join)
+		return true;
+
+	/*
+	 * The only relation type that can help us is a base rel. If the relation
+	 * does not have any eclass joins then we'll assume that it's not an
+	 * equality type join, therefore won't be provably unique.
+	 */
+	if (targetrel->reloptkind != RELOPT_BASEREL ||
+		targetrel->has_eclass_joins == false)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the rel.
+	 */
+	if (targetrel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation rel, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's no point in going
+		 * further.
+		 */
+		if (targetrel->indexlist == NIL)
+			return false;
+	}
+	else if (targetrel->rtekind == RTE_SUBQUERY)
+	{
+		subquery = root->simple_rte_array[targetrtr->rtindex]->subquery;
+
+		/*
+		 * If the subquery has no qualities that support distinctness proofs
+		 * then there's no point in going further.
+		 */
+		if (!query_supports_distinctness(subquery))
+			return false;
+	}
+	else
+		return false;			/* unsupported rtekind */
+
+	/*
+	 * We now search through each relation in the join list searching for every
+	 * relation which joins to idxrel. Our goal here is to determine if idxrel
+	 * will match at most 1 row for each of the relations it joins to. In order
+	 * to prove this we'll analyze unique indexes for plain relations, or if
+	 * it's a subquery we'll look at GROUP BY and DISTINCT clauses.
+	 */
+
+	foreach (lc, joinlist)
+	{
+		RangeTblRef	   *rtr = (RangeTblRef *) lfirst(lc);
+		RelOptInfo	   *rel;
+		ListCell	   *lc2;
+		List		   *index_exprs;
+		List		   *operator_list;
+		bool			foundjoin = false;
+
+		/* Skip ourself, and anything that's not a RangeTblRef */
+		if (rtr == targetrtr || !IsA(rtr, RangeTblRef))
+			continue;
+
+		rel = find_base_rel(root, rtr->rtindex);
+
+		/* No point in checking if this rel has no eclass joins */
+		if (rel->has_eclass_joins == false)
+			continue;
+
+		index_exprs = NIL;
+		operator_list = NIL;
+
+		/*
+		 * We now analyze each eclass and extract a list of columns which the
+		 * targetrel relation must be unique over.  There are cases here that
+		 * we don't properly handle, such as if more than 1 Var from either the
+		 * targetrel, or rel exist in the same eclass. In this case it's not
+		 * clear which equality operators we'd demand that the index used. We
+		 * would also need to handle passing alternative lists of columns to
+		 * check the index against. For now this seems more hassle than it's
+		 * worth, and these are likely quite rare in the real world.
+		 */
+		foreach(lc2, root->eq_classes)
+		{
+			EquivalenceClass *ec = (EquivalenceClass *) lfirst(lc2);
+
+			/*
+			 * Any eclasses that don't have at least 2 members or have
+			 * volatile expressions are useless to us. Skip these.
+			 */
+			if (list_length(ec->ec_members) < 2 ||
+				ec->ec_has_volatile)
+				continue;
+
+			/* Does this eclass have members for both these relations? */
+			if (bms_overlap(targetrel->relids, ec->ec_relids) &&
+				bms_overlap(rel->relids, ec->ec_relids))
+			{
+				ListCell *lc3;
+				EquivalenceMember *relecm = NULL;
+				EquivalenceMember *targetrelecm = NULL;
+
+				foundjoin = true;
+
+				foreach (lc3, ec->ec_members)
+				{
+					EquivalenceMember	*ecm = (EquivalenceMember *) lfirst(lc3);
+					int					 ecmrel;
+
+					/*
+					 * We're only interested in ec members for a single rel.
+					 * Expressions such as t1.col + t2.col or function calls
+					 * where the parameters come from more than 1 rel are out.
+					 */
+					if (!bms_get_singleton_member(ecm->em_relids, &ecmrel))
+						continue;
+
+					if (ecmrel == rel->relid)
+					{
+						if (relecm != NULL)
+							return false;
+						relecm = ecm;
+					}
+
+					else if (ecmrel == targetrel->relid)
+					{
+						if (targetrelecm != NULL)
+							return false;
+						targetrelecm = ecm;
+					}
+				}
+
+				/*
+				 * We did make a quick check above to ensure that this eclass
+				 * contained members for both relations, but we could have
+				 * skipped some members which contained some expression which
+				 * referenced both relations. So despite our pre-check, the if
+				 * test below could still be false.
+				 */
+				if (relecm != NULL && targetrelecm != NULL)
+				{
+					Oid opno = select_equality_operator(ec,
+							relecm->em_datatype, targetrelecm->em_datatype);
+
+					if (!OidIsValid(opno))
+						return false;
+
+					index_exprs = lappend(index_exprs, targetrelecm->em_expr);
+					operator_list = lappend_oid(operator_list, opno);
+				}
+			}
+		}
+
+		/*
+		 * If we found no suitable eclass members then these 2 relations either
+		 * have no join condition between them or we found a join condition but
+		 * there was something about it that's not suitable for testing for
+		 * unique properties, in the former case we're safe to skip to the next
+		 * relation in the latter case we'd better bail.
+		 */
+		if (index_exprs == NIL)
+		{
+			if (foundjoin)
+				return false;		/* join condition not suitable */
+
+			continue;
+		}
+
+		if (targetrel->rtekind == RTE_RELATION)
+		{
+			if (!relation_has_unique_index_for(root, targetrel, NIL, index_exprs, operator_list))
+				return false;
+
+			/* if it's unique then we'll also need to check other relations */
+		}
+		else /* RTE_SUBQUERY */
+		{
+			List	   *colnos = NIL;
+			ListCell   *l;
+
+			/*
+			 * Build the argument lists for query_is_distinct_for: a list of
+			 * output column numbers that the query needs to be distinct over.
+			 * Possibly we could do something with expressions in the subquery
+			 * outputs, too, but for now keep it simple.
+			 */
+			foreach(l, index_exprs)
+			{
+				Var		   *var = (Var *) lfirst(l);
+
+				if (!IsA(var, Var))
+					return false; /* punt if we have a non-Var */
+
+				colnos = lappend_int(colnos, var->varattno);
+			}
+
+			if (!query_is_distinct_for(subquery, colnos, operator_list))
+				return false;
+
+			/* if it's unique then we'll also need to check other relations */
+		}
+	}
+
+	return true;	/* all joins proved to be unique */
+}
+
+/*
+ * Returns True if it can be proved that this special join will never produce
+ * more than 1 row per outer row, otherwise returns false if there is
+ * insufficient evidence to prove the join is unique. uniquerel is always
+ * set to the RelOptInfo of the rel that was proved to be unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo,
+						   RelOptInfo **uniquerel)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Query	   *subquery = NULL;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	/* check if we've already marked this join as unique on a previous call */
+	if (innerrel->is_unique_join)
+	{
+		*uniquerel = innerrel;
+		return true;
+	}
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (innerrel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation innerrel, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's no point in going
+		 * further.
+		 */
+		if (innerrel->indexlist == NIL)
+			return false;
+	}
+	else if (innerrel->rtekind == RTE_SUBQUERY)
+	{
+		subquery = root->simple_rte_array[innerrelid]->subquery;
+
+		/*
+		 * If the subquery has no qualities that support distinctness proofs
+		 * then there's no point in going further.
+		 */
+		if (!query_supports_distinctness(subquery))
+			return false;
+	}
+	else
+		return false;			/* unsupported rtekind */
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -274,10 +589,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we can't
+			 * mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -310,7 +623,10 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	{
 		/* Now examine the indexes to see if we have a matching unique index */
 		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
+		{
+			*uniquerel = innerrel;
 			return true;
+		}
 	}
 	else	/* innerrel->rtekind == RTE_SUBQUERY */
 	{
@@ -358,7 +674,10 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		}
 
 		if (query_is_distinct_for(subquery, colnos, opids))
+		{
+			*uniquerel = innerrel;
 			return true;
+		}
 	}
 
 	/*
@@ -368,7 +687,6 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	return false;
 }
 
-
 /*
  * Remove the target relid from the planner's data structures, having
  * determined that there is no need to include it in the query.
@@ -576,14 +894,15 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
  * query_is_distinct_for()'s argument lists if the call could not possibly
  * succeed.
  */
-bool
+static bool
 query_supports_distinctness(Query *query)
 {
 	if (query->distinctClause != NIL ||
 		query->groupClause != NIL ||
 		query->hasAggs ||
 		query->havingQual ||
-		query->setOperations)
+		query->setOperations ||
+		query->jointree->fromlist == NIL)
 		return true;
 
 	return false;
@@ -607,7 +926,7 @@ query_supports_distinctness(Query *query)
  * should give trustworthy answers for all operators that we might need
  * to deal with here.)
  */
-bool
+static bool
 query_is_distinct_for(Query *query, List *colnos, List *opids)
 {
 	ListCell   *l;
@@ -676,6 +995,10 @@ query_is_distinct_for(Query *query, List *colnos, List *opids)
 		 */
 		if (query->hasAggs || query->havingQual)
 			return true;
+
+		/* tables with an empty from clause should return at most 1 row */
+		if (query->jointree->fromlist == NIL)
+			return true;
 	}
 
 	/*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index cb69c03..94bf23f 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -132,12 +132,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinType jointype, bool unique_inner);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinType jointype, bool unique_inner);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -152,7 +152,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinType jointype, bool unique_inner);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2192,7 +2192,8 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path->jointype,
+							  best_path->unique_inner);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->path);
 
@@ -2486,7 +2487,8 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   best_path->jpath.jointype,
+							   best_path->jpath.unique_inner);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
@@ -2612,7 +2614,8 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  best_path->jpath.jointype,
+							  best_path->jpath.unique_inner);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3717,7 +3720,8 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinType jointype,
+			  bool unique_inner)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3728,6 +3732,7 @@ make_nestloop(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->join.jointype = jointype;
+	node->join.unique_inner = unique_inner;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
 
@@ -3741,7 +3746,8 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinType jointype,
+			  bool unique_inner)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3753,6 +3759,7 @@ make_hashjoin(List *tlist,
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
 	node->join.jointype = jointype;
+	node->join.unique_inner = unique_inner;
 	node->join.joinqual = joinclauses;
 
 	return node;
@@ -3801,7 +3808,8 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinType jointype,
+			   bool unique_inner)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3817,6 +3825,7 @@ make_mergejoin(List *tlist,
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
 	node->join.jointype = jointype;
+	node->join.unique_inner = unique_inner;
 	node->join.joinqual = joinclauses;
 
 	return node;
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 848df97..9fcd047 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -175,6 +175,12 @@ query_planner(PlannerInfo *root, List *tlist,
 	fix_placeholder_input_needed_levels(root);
 
 	/*
+	 * Analyze all joins and mark which ones can produce at most 1 row
+	 * for each outer row.
+	 */
+	mark_unique_joins(root, joinlist);
+
+	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
 	 * until we've built baserel data structures and classified qual clauses.
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index faca30b..f87e25f 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1128,15 +1128,10 @@ create_unique_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
 	pathnode->uniq_exprs = sjinfo->semi_rhs_exprs;
 
 	/*
-	 * If the input is a relation and it has a unique index that proves the
-	 * semi_rhs_exprs are unique, then we don't need to do anything.  Note
-	 * that relation_has_unique_index_for automatically considers restriction
-	 * clauses for the rel, as well.
+	 * If the input is a relation or subquery was discovered to have unique
+	 * properties then the path is unique already.
 	 */
-	if (rel->rtekind == RTE_RELATION && sjinfo->semi_can_btree &&
-		relation_has_unique_index_for(root, rel, NIL,
-									  sjinfo->semi_rhs_exprs,
-									  sjinfo->semi_operators))
+	if (rel->is_unique_join)
 	{
 		pathnode->umethod = UNIQUE_PATH_NOOP;
 		pathnode->path.rows = rel->rows;
@@ -1151,46 +1146,6 @@ create_unique_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
 		return pathnode;
 	}
 
-	/*
-	 * If the input is a subquery whose output must be unique already, then we
-	 * don't need to do anything.  The test for uniqueness has to consider
-	 * exactly which columns we are extracting; for example "SELECT DISTINCT
-	 * x,y" doesn't guarantee that x alone is distinct. So we cannot check for
-	 * this optimization unless semi_rhs_exprs consists only of simple Vars
-	 * referencing subquery outputs.  (Possibly we could do something with
-	 * expressions in the subquery outputs, too, but for now keep it simple.)
-	 */
-	if (rel->rtekind == RTE_SUBQUERY)
-	{
-		RangeTblEntry *rte = planner_rt_fetch(rel->relid, root);
-
-		if (query_supports_distinctness(rte->subquery))
-		{
-			List	   *sub_tlist_colnos;
-
-			sub_tlist_colnos = translate_sub_tlist(sjinfo->semi_rhs_exprs,
-												   rel->relid);
-
-			if (sub_tlist_colnos &&
-				query_is_distinct_for(rte->subquery,
-									  sub_tlist_colnos,
-									  sjinfo->semi_operators))
-			{
-				pathnode->umethod = UNIQUE_PATH_NOOP;
-				pathnode->path.rows = rel->rows;
-				pathnode->path.startup_cost = subpath->startup_cost;
-				pathnode->path.total_cost = subpath->total_cost;
-				pathnode->path.pathkeys = subpath->pathkeys;
-
-				rel->cheapest_unique_path = (Path *) pathnode;
-
-				MemoryContextSwitchTo(oldcontext);
-
-				return pathnode;
-			}
-		}
-	}
-
 	/* Estimate number of output rows */
 	pathnode->path.rows = estimate_num_groups(root,
 											  sjinfo->semi_rhs_exprs,
@@ -1579,6 +1534,7 @@ create_nestloop_path(PlannerInfo *root,
 								  &restrict_clauses);
 	pathnode->path.pathkeys = pathkeys;
 	pathnode->jointype = jointype;
+	pathnode->unique_inner = inner_path->parent->is_unique_join;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
@@ -1636,6 +1592,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  &restrict_clauses);
 	pathnode->jpath.path.pathkeys = pathkeys;
 	pathnode->jpath.jointype = jointype;
+	pathnode->jpath.unique_inner = inner_path->parent->is_unique_join;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
@@ -1704,6 +1661,7 @@ create_hashjoin_path(PlannerInfo *root,
 	 */
 	pathnode->jpath.path.pathkeys = NIL;
 	pathnode->jpath.jointype = jointype;
+	pathnode->jpath.unique_inner = inner_path->parent->is_unique_join;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 8cfbea0..c20b62b 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -128,6 +128,7 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptKind reloptkind)
 	rel->baserestrictcost.per_tuple = 0;
 	rel->joininfo = NIL;
 	rel->has_eclass_joins = false;
+	rel->is_unique_join = false;
 
 	/* Check type of rtable entry */
 	switch (rte->rtekind)
@@ -390,6 +391,7 @@ build_join_rel(PlannerInfo *root,
 	joinrel->baserestrictcost.per_tuple = 0;
 	joinrel->joininfo = NIL;
 	joinrel->has_eclass_joins = false;
+	joinrel->is_unique_join = false;
 
 	/*
 	 * Create a new tlist containing just the vars that need to be output from
@@ -701,6 +703,7 @@ build_empty_join_rel(PlannerInfo *root)
 	joinrel->rows = 1;			/* we produce one row for such cases */
 	joinrel->width = 0;			/* it contains no Vars */
 	joinrel->rtekind = RTE_JOIN;
+	joinrel->is_unique_join = false;
 
 	root->join_rel_list = lappend(root->join_rel_list, joinrel);
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 59b17f3..805653d 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1561,6 +1561,7 @@ typedef struct JoinState
 {
 	PlanState	ps;
 	JoinType	jointype;
+	bool		unique_inner;	/* Inner side will match 0 or 1 outer rows */
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
 } JoinState;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index f6683f0..0b40f41 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -525,9 +525,12 @@ typedef struct CustomScan
 /* ----------------
  *		Join node
  *
- * jointype:	rule for joining tuples from left and right subtrees
- * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
- *				(plan.qual contains conditions that came from WHERE)
+ * jointype:		rule for joining tuples from left and right subtrees
+ * unique_inner:	true if the planner discovered that each inner tuple
+ *					can only match at most 1 outer tuple. Proofs include
+ *					UNIQUE indexes and GROUP BY/DISTINCT clauses etc.
+ * joinqual:		qual conditions that came from JOIN/ON or JOIN/USING
+ *					(plan.qual contains conditions that came from WHERE)
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -542,6 +545,7 @@ typedef struct Join
 {
 	Plan		plan;
 	JoinType	jointype;
+	bool		unique_inner;	/* Inner side will match 0 or 1 outer rows */
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
 } Join;
 
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 334cf51..1302a88 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -470,6 +470,8 @@ typedef struct RelOptInfo
 	List	   *joininfo;		/* RestrictInfo structures for join clauses
 								 * involving this rel */
 	bool		has_eclass_joins;		/* T means joininfo is incomplete */
+	bool		is_unique_join;		/* True if this rel won't contain more
+									 * than 1 row for the join condition */
 } RelOptInfo;
 
 /*
@@ -1028,6 +1030,8 @@ typedef struct JoinPath
 
 	JoinType	jointype;
 
+	bool		unique_inner;	/* inner side of join is unique */
+
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
 
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 6cad92e..74f3a1f 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -119,6 +119,8 @@ extern List *generate_join_implied_equalities(PlannerInfo *root,
 								 Relids join_relids,
 								 Relids outer_relids,
 								 RelOptInfo *inner_rel);
+extern Oid select_equality_operator(EquivalenceClass *ec, Oid lefttype,
+								 Oid righttype);
 extern bool exprs_known_equal(PlannerInfo *root, Node *item1, Node *item2);
 extern void add_child_rel_equivalences(PlannerInfo *root,
 						   AppendRelInfo *appinfo,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index fa72918..54edb9f 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -122,9 +122,8 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
-extern bool query_supports_distinctness(Query *query);
-extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
 /*
  * prototypes for plan/setrefs.c
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 57fc910..cb3db4a 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3117,6 +3117,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Unique Join: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3124,12 +3125,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Unique Join: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3216,7 +3218,7 @@ select id from a where id in (
 );
          QUERY PLAN         
 ----------------------------
- Hash Semi Join
+ Hash Join
    Hash Cond: (a.id = b.id)
    ->  Seq Scan on a
    ->  Hash
@@ -3413,10 +3415,10 @@ SELECT * FROM
   ON true;
  x |        q1        |        q2         |        y         
 ---+------------------+-------------------+------------------
- 1 |              123 |               456 |              123
- 1 |              123 |  4567890123456789 |              123
  1 | 4567890123456789 |               123 |               42
  1 | 4567890123456789 |  4567890123456789 | 4567890123456789
+ 1 |              123 |  4567890123456789 |              123
+ 1 |              123 |               456 |              123
  1 | 4567890123456789 | -4567890123456789 | 4567890123456789
 (5 rows)
 
@@ -3984,12 +3986,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4016,12 +4019,13 @@ select * from
 ----------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, 42::bigint))
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, 42::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4049,6 +4053,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Unique Join: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4056,7 +4061,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4076,12 +4081,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Unique Join: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4103,10 +4109,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Unique Join: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Unique Join: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4115,7 +4123,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4161,10 +4169,12 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Unique Join: No
          Join Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
@@ -4172,7 +4182,7 @@ select * from
                Output: c.q1
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1
-(13 rows)
+(15 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -4239,13 +4249,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, 42::bigint)), d.q1, (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, 42::bigint)), d.q2)))
+   Unique Join: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, 42::bigint)), (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2))
+         Unique Join: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, 42::bigint)), (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2))
+               Unique Join: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, 42::bigint))
+                     Unique Join: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -4261,7 +4275,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, 42::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -4279,17 +4293,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Unique Join: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Unique Join: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Unique Join: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Unique Join: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Unique Join: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -4311,7 +4330,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -4324,16 +4343,20 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Unique Join: Yes
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Unique Join: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
-   ->  Result
-         Output: 3
-(11 rows)
+   ->  Materialize
+         Output: (3)
+         ->  Result
+               Output: 3
+(15 rows)
 
 -- test some error cases where LATERAL should have been used but wasn't
 select f1,g from int4_tbl a, (select f1 as g) ss;
@@ -4416,3 +4439,285 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Unique Join: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Join: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Unique Join: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Join: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Join: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Join: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Unique Join: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Join: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(13 rows)
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Join: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(13 rows)
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+          QUERY PLAN          
+------------------------------
+ Nested Loop
+   Output: j1.id, (1)
+   Unique Join: Yes
+   Join Filter: (j1.id = (1))
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: (1)
+         ->  Result
+               Output: 1
+(10 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Unique Join: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Join: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Join: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+-- ensure no unique joins when the tables are joined in 3 ways.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id2 and j1.id2 = j2.id2
+inner join j3 on j1.id1 = j3.id1 and j2.id2 = j3.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2, j3.id1, j3.id2
+   Unique Join: No
+   Join Filter: (j1.id1 = j3.id1)
+   ->  Nested Loop
+         Output: j1.id1, j1.id2, j2.id1, j2.id2
+         Unique Join: No
+         Join Filter: (j1.id1 = j2.id2)
+         ->  Seq Scan on public.j1
+               Output: j1.id1, j1.id2
+               Filter: (j1.id1 = j1.id2)
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j3
+         Output: j3.id1, j3.id2
+         Filter: (j3.id1 = j3.id2)
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 6dabe50..4378e9d 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2016,12 +2016,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2042,11 +2043,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Unique Join: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index b14410f..7f43784 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -761,6 +761,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Unique Join: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -769,7 +770,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -789,6 +790,7 @@ select * from int4_tbl o where (f1, f1) in
 ----------------------------------------------------------------------
  Hash Join
    Output: o.f1
+   Unique Join: No
    Hash Cond: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -805,7 +807,7 @@ select * from int4_tbl o where (f1, f1) in
                            Group Key: i.f1
                            ->  Seq Scan on public.int4_tbl i
                                  Output: i.f1
-(18 rows)
+(19 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index c49e769..b481cda 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2071,6 +2071,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 3) AND leakproof(t1_5.a))
@@ -2088,6 +2089,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t11
                            Output: t11.ctid, t11.a, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 3) AND leakproof(t11.a))
@@ -2105,6 +2107,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 3) AND leakproof(t12_2.a))
@@ -2122,6 +2125,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 3) AND leakproof(t111_3.a))
@@ -2132,7 +2136,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 3)
-(69 rows)
+(73 rows)
 
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
 SELECT * FROM v1 WHERE a=100; -- Nothing should have been changed to 100
@@ -2157,6 +2161,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 8) AND leakproof(t1_5.a))
@@ -2174,6 +2179,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t11
                            Output: t11.a, t11.ctid, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 8) AND leakproof(t11.a))
@@ -2191,6 +2197,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 8) AND leakproof(t12_2.a))
@@ -2208,6 +2215,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Join: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 8) AND leakproof(t111_3.a))
@@ -2218,7 +2226,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 8)
-(69 rows)
+(73 rows)
 
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
 NOTICE:  snooped value: 8
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 524e0ef..1eb2da4 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2093,6 +2093,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: 42::bigint, 47::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Unique Join: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2100,6 +2101,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Unique Join: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2107,6 +2109,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Unique Join: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2114,12 +2117,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Unique Join: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(34 rows)
+(38 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 06a27ea..34f1dc4 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1307,3 +1307,104 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+-- ensure no unique joins when the tables are joined in 3 ways.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id2 and j1.id2 = j2.id2
+inner join j3 on j1.id1 = j3.id1 and j2.id2 = j3.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#19Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: David Rowley (#18)
Re: Performance improvement for joins where outer side is unique

Hello, I don't have enough time for now but made some
considerations on this.

It might be a way that marking unique join peer at bottom and
propagate up, not searching from top of join list.
Around create_join_clause might be a candidate for it.
I'll investigate that later.

regards,

At Sat, 14 Mar 2015 23:05:24 +1300, David Rowley <dgrowleyml@gmail.com> wrote in <CAApHDvoh-EKF51QQyNoJUe0eHYMZw6OzJjjgYP63Cmw7QfebjA@mail.gmail.com>
dgrowleyml> On 14 March 2015 at 14:51, David Rowley <dgrowleyml@gmail.com> wrote:
dgrowleyml>
dgrowleyml> > On 13 March 2015 at 20:34, Kyotaro HORIGUCHI <
dgrowleyml> > horiguchi.kyotaro@lab.ntt.co.jp> wrote:
dgrowleyml> >
dgrowleyml> >> Unfortunately I can't decide this to be 'ready for commiter' for
dgrowleyml> >>
dgrowleyml> > now. I think we should have this on smaller footprint, in a
dgrowleyml> >> method without separate exhauxtive searching. I also had very
dgrowleyml> >> similar problem in the past but I haven't find such a way for my
dgrowleyml> >> problem..
dgrowleyml> >>
dgrowleyml> >>
dgrowleyml> > I don't think it's ready yet either. I've just been going over a few
dgrowleyml> > things and looking at Tom's recent commit b557226 in pathnode.c I've got a
dgrowleyml> > feeling that this patch would need to re-factor some code that's been
dgrowleyml> > modified around the usage of relation_has_unique_index_for() as when this
dgrowleyml> > code is called, the semi joins have already been analysed to see if they're
dgrowleyml> > unique, so it could be just a case of ripping all of that out
dgrowleyml> > of create_unique_path() and just putting a check to say rel->is_unique_join
dgrowleyml> > in there. But if I do that then I'm not quite sure if
dgrowleyml> > SpecialJoinInfo->semi_rhs_exprs and SpecialJoinInfo->semi_operators would
dgrowleyml> > still be needed at all. These were only added in b557226. Changing this
dgrowleyml> > would help reduce the extra planning time when the query contains
dgrowleyml> > semi-joins. To be quite honest, this type of analysis belongs in
dgrowleyml> > analyzejoin.c anyway. I'm tempted to hack at this part some more, but I'd
dgrowleyml> > rather Tom had a quick glance at what I'm trying to do here first.
dgrowleyml> >
dgrowleyml> >
dgrowleyml>
dgrowleyml> I decided to hack away any change the code Tom added in b557226. I've
dgrowleyml> changed it so that create_unique_path() now simply just uses if
dgrowleyml> (rel->is_unique_join), instead off all the calls to
dgrowleyml> relation_has_unique_index_for() and query_is_distinct_for(). This vastly
dgrowleyml> simplifies that code. One small change is that Tom's checks for uniqueness
dgrowleyml> on semi joins included checks for volatile functions, this check didn't
dgrowleyml> exist in the original join removal code, so I've left it out. We'll never
dgrowleyml> match a expression with a volatile function to a unique index as indexes
dgrowleyml> don't allow volatile function expressions anyway. So as I understand it
dgrowleyml> this only serves as a fast path out if the join condition has a volatile
dgrowleyml> function... But I'd assume that check is not all that cheap.
dgrowleyml>
dgrowleyml> I ended up making query_supports_distinctness() and query_is_distinct_for()
dgrowleyml> static in analyzejoins.c as they're not used in any other files.
dgrowleyml> relation_has_unique_index_for() is also now only used in analyzejoins.c,
dgrowleyml> but I've not moved it into that file yet as I don't want to bloat the
dgrowleyml> patch. I just added a comment to say it needs moved.
dgrowleyml>
dgrowleyml> I've also added a small amount of code to query_is_distinct_for() which
dgrowleyml> allows subqueries such as (select 1 a offset 0) to be marked as unique. I
dgrowleyml> thought it was a little silly that these were not being detected as unique,
dgrowleyml> so I fixed it. This has the side effect of allowing left join removals for
dgrowleyml> queries such as: select t1.* from t1 left join (select 1 a offset 0) a on
dgrowleyml> t1.id=a.a;
dgrowleyml>
dgrowleyml> Updated patch attached.
dgrowleyml>
dgrowleyml> Regards
dgrowleyml>
dgrowleyml> David Rowley

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

#20Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: Kyotaro HORIGUCHI (#19)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

Hello, The attached is non-search version of unique join.

It is not fully examined but looks to work as expected. Many
small changes make the patch larger but affected area is rather
small.

What do you think about this?

Hello, I don't have enough time for now but made some
considerations on this.

It might be a way that marking unique join peer at bottom and
propagate up, not searching from top of join list.
Around create_join_clause might be a candidate for it.
I'll investigate that later.

regards,

--
Kyotaro Horiguchi
NTT Open Source Software Center

Attachments:

unique_join_horiguti_v0.patchtext/x-patch; charset=us-asciiDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a951c55..b8a68b5 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1151,9 +1151,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
 						appendStringInfo(es->str, " %s Join", jointype);
 					else if (!IsA(plan, NestLoop))
 						appendStringInfoString(es->str, " Join");
+					if (((Join *)plan)->inner_unique)
+						appendStringInfoString(es->str, "(inner unique)");
+
 				}
 				else
+				{
 					ExplainPropertyText("Join Type", jointype, es);
+					ExplainPropertyText("Inner unique", 
+							((Join *)plan)->inner_unique?"true":"false", es);
+				}
 			}
 			break;
 		case T_SetOp:
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 1d78cdf..d3b14e5 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,11 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.inner_unique)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -451,6 +452,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate = makeNode(HashJoinState);
 	hjstate->js.ps.plan = (Plan *) node;
 	hjstate->js.ps.state = estate;
+	hjstate->js.inner_unique = node->join.inner_unique;
 
 	/*
 	 * Miscellaneous initialization
@@ -498,8 +500,10 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			hjstate->js.inner_unique = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 15742c5..3c21ffe 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,11 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.inner_unique)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1486,6 +1487,8 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.plan = (Plan *) node;
 	mergestate->js.ps.state = estate;
 
+	mergestate->js.inner_unique = node->join.inner_unique;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -1553,8 +1556,10 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			mergestate->js.inner_unique = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e66bcda..342c448 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * We'll consider returning the first match if the inner is
+			 * unique, but after that we're done with this outer tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.inner_unique)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -310,6 +310,8 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.plan = (Plan *) node;
 	nlstate->js.ps.state = estate;
 
+	nlstate->js.inner_unique = node->join.inner_unique;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -354,8 +356,10 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			nlstate->js.inner_unique = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 1da953f..8363216 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -26,18 +26,21 @@
 	((path)->param_info && bms_overlap(PATH_REQ_OUTER(path), (rel)->relids))
 
 static void sort_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
-					 RelOptInfo *outerrel, RelOptInfo *innerrel,
+					 RelOptInfo *outerrel,
+					 RelOptInfo *innerrel, bool inner_unique,
 					 List *restrictlist, List *mergeclause_list,
 					 JoinType jointype, SpecialJoinInfo *sjinfo,
 					 Relids param_source_rels, Relids extra_lateral_rels);
 static void match_unsorted_outer(PlannerInfo *root, RelOptInfo *joinrel,
-					 RelOptInfo *outerrel, RelOptInfo *innerrel,
+					 RelOptInfo *outerrel,
+					 RelOptInfo *innerrel, bool inner_unique,
 					 List *restrictlist, List *mergeclause_list,
 					 JoinType jointype, SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
 					 Relids param_source_rels, Relids extra_lateral_rels);
 static void hash_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
-					 RelOptInfo *outerrel, RelOptInfo *innerrel,
+					 RelOptInfo *outerrel,
+					 RelOptInfo *innerrel, bool inner_unique,
 					 List *restrictlist,
 					 JoinType jointype, SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -49,7 +52,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+										   RelOptInfo *innerrel);
 
 /*
  * add_paths_to_joinrel
@@ -89,6 +93,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	Relids		param_source_rels = NULL;
 	Relids		extra_lateral_rels = NULL;
 	ListCell   *lc;
+	bool		inner_unique = false;
 
 	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
@@ -115,6 +120,31 @@ add_paths_to_joinrel(PlannerInfo *root,
 									   &semifactors);
 
 	/*
+	 * We can optimize inner loop execution for joins on which the inner rel
+	 * is unique on the restrictlist.
+	 */
+	if (jointype == JOIN_INNER &&
+ 		innerrel->rtekind == RTE_RELATION &&
+		restrictlist)
+	{
+		/* relation_has_unique_index_for adds some restrictions */
+		int org_len = list_length(restrictlist);
+		ListCell *lc;
+
+		foreach (lc, restrictlist)
+		{
+			clause_sides_match_join((RestrictInfo *) lfirst(lc),
+									outerrel, innerrel);
+		}
+		if (relation_has_unique_index_for(root, innerrel, restrictlist,
+										  NIL, NIL))
+			inner_unique = true;
+
+		/* Remove restirictions added by the function */
+		list_truncate(restrictlist, org_len);
+	}
+
+	/*
 	 * Decide whether it's sensible to generate parameterized paths for this
 	 * joinrel, and if so, which relations such paths should require.  There
 	 * is usually no need to create a parameterized result path unless there
@@ -212,7 +242,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	 * sorted.  Skip this if we can't mergejoin.
 	 */
 	if (mergejoin_allowed)
-		sort_inner_and_outer(root, joinrel, outerrel, innerrel,
+		sort_inner_and_outer(root, joinrel, outerrel, innerrel, inner_unique,
 							 restrictlist, mergeclause_list, jointype,
 							 sjinfo,
 							 param_source_rels, extra_lateral_rels);
@@ -225,7 +255,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	 * joins at all, so it wouldn't work in the prohibited cases either.)
 	 */
 	if (mergejoin_allowed)
-		match_unsorted_outer(root, joinrel, outerrel, innerrel,
+		match_unsorted_outer(root, joinrel, outerrel, innerrel, inner_unique,
 							 restrictlist, mergeclause_list, jointype,
 							 sjinfo, &semifactors,
 							 param_source_rels, extra_lateral_rels);
@@ -256,7 +286,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	 * joins, because there may be no other alternative.
 	 */
 	if (enable_hashjoin || jointype == JOIN_FULL)
-		hash_inner_and_outer(root, joinrel, outerrel, innerrel,
+		hash_inner_and_outer(root, joinrel, outerrel, innerrel, inner_unique,
 							 restrictlist, jointype,
 							 sjinfo, &semifactors,
 							 param_source_rels, extra_lateral_rels);
@@ -277,6 +307,7 @@ try_nestloop_path(PlannerInfo *root,
 				  Relids extra_lateral_rels,
 				  Path *outer_path,
 				  Path *inner_path,
+				  bool  inner_unique,
 				  List *restrict_clauses,
 				  List *pathkeys)
 {
@@ -349,6 +380,7 @@ try_nestloop_path(PlannerInfo *root,
 									  semifactors,
 									  outer_path,
 									  inner_path,
+									  inner_unique,
 									  restrict_clauses,
 									  pathkeys,
 									  required_outer));
@@ -374,6 +406,7 @@ try_mergejoin_path(PlannerInfo *root,
 				   Relids extra_lateral_rels,
 				   Path *outer_path,
 				   Path *inner_path,
+				   bool	 inner_unique,
 				   List *restrict_clauses,
 				   List *pathkeys,
 				   List *mergeclauses,
@@ -434,6 +467,7 @@ try_mergejoin_path(PlannerInfo *root,
 									   sjinfo,
 									   outer_path,
 									   inner_path,
+									   inner_unique,
 									   restrict_clauses,
 									   pathkeys,
 									   required_outer,
@@ -463,6 +497,7 @@ try_hashjoin_path(PlannerInfo *root,
 				  Relids extra_lateral_rels,
 				  Path *outer_path,
 				  Path *inner_path,
+				  bool  inner_unique,
 				  List *restrict_clauses,
 				  List *hashclauses)
 {
@@ -510,6 +545,7 @@ try_hashjoin_path(PlannerInfo *root,
 									  semifactors,
 									  outer_path,
 									  inner_path,
+									  inner_unique,
 									  restrict_clauses,
 									  required_outer,
 									  hashclauses));
@@ -574,6 +610,7 @@ sort_inner_and_outer(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 RelOptInfo *outerrel,
 					 RelOptInfo *innerrel,
+					 bool inner_unique,
 					 List *restrictlist,
 					 List *mergeclause_list,
 					 JoinType jointype,
@@ -629,6 +666,7 @@ sort_inner_and_outer(PlannerInfo *root,
 												 inner_path, sjinfo);
 		Assert(inner_path);
 		jointype = JOIN_INNER;
+		inner_unique = true;
 	}
 
 	/*
@@ -712,6 +750,7 @@ sort_inner_and_outer(PlannerInfo *root,
 						   extra_lateral_rels,
 						   outer_path,
 						   inner_path,
+						   inner_unique,
 						   restrictlist,
 						   merge_pathkeys,
 						   cur_mergeclauses,
@@ -762,6 +801,7 @@ match_unsorted_outer(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 RelOptInfo *outerrel,
 					 RelOptInfo *innerrel,
+					 bool  inner_unique,
 					 List *restrictlist,
 					 List *mergeclause_list,
 					 JoinType jointype,
@@ -832,6 +872,7 @@ match_unsorted_outer(PlannerInfo *root,
 		inner_cheapest_total = (Path *)
 			create_unique_path(root, innerrel, inner_cheapest_total, sjinfo);
 		Assert(inner_cheapest_total);
+		inner_unique = true;
 	}
 	else if (nestjoinOK)
 	{
@@ -901,6 +942,7 @@ match_unsorted_outer(PlannerInfo *root,
 							  extra_lateral_rels,
 							  outerpath,
 							  inner_cheapest_total,
+							  inner_unique,
 							  restrictlist,
 							  merge_pathkeys);
 		}
@@ -927,6 +969,7 @@ match_unsorted_outer(PlannerInfo *root,
 								  extra_lateral_rels,
 								  outerpath,
 								  innerpath,
+								  inner_unique,
 								  restrictlist,
 								  merge_pathkeys);
 			}
@@ -942,6 +985,7 @@ match_unsorted_outer(PlannerInfo *root,
 								  extra_lateral_rels,
 								  outerpath,
 								  matpath,
+								  inner_unique,
 								  restrictlist,
 								  merge_pathkeys);
 		}
@@ -998,6 +1042,7 @@ match_unsorted_outer(PlannerInfo *root,
 						   extra_lateral_rels,
 						   outerpath,
 						   inner_cheapest_total,
+						   inner_unique,
 						   restrictlist,
 						   merge_pathkeys,
 						   mergeclauses,
@@ -1097,6 +1142,7 @@ match_unsorted_outer(PlannerInfo *root,
 								   extra_lateral_rels,
 								   outerpath,
 								   innerpath,
+								   inner_unique,
 								   restrictlist,
 								   merge_pathkeys,
 								   newclauses,
@@ -1143,6 +1189,7 @@ match_unsorted_outer(PlannerInfo *root,
 									   extra_lateral_rels,
 									   outerpath,
 									   innerpath,
+									   inner_unique,
 									   restrictlist,
 									   merge_pathkeys,
 									   newclauses,
@@ -1182,6 +1229,7 @@ hash_inner_and_outer(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 RelOptInfo *outerrel,
 					 RelOptInfo *innerrel,
+					 bool inner_unique,
 					 List *restrictlist,
 					 JoinType jointype,
 					 SpecialJoinInfo *sjinfo,
@@ -1264,6 +1312,7 @@ hash_inner_and_outer(PlannerInfo *root,
 							  extra_lateral_rels,
 							  cheapest_total_outer,
 							  cheapest_total_inner,
+							  inner_unique,
 							  restrictlist,
 							  hashclauses);
 			/* no possibility of cheap startup here */
@@ -1284,6 +1333,7 @@ hash_inner_and_outer(PlannerInfo *root,
 							  extra_lateral_rels,
 							  cheapest_total_outer,
 							  cheapest_total_inner,
+							  true,
 							  restrictlist,
 							  hashclauses);
 			if (cheapest_startup_outer != NULL &&
@@ -1297,6 +1347,7 @@ hash_inner_and_outer(PlannerInfo *root,
 								  extra_lateral_rels,
 								  cheapest_startup_outer,
 								  cheapest_total_inner,
+								  true,
 								  restrictlist,
 								  hashclauses);
 		}
@@ -1322,6 +1373,7 @@ hash_inner_and_outer(PlannerInfo *root,
 								  extra_lateral_rels,
 								  cheapest_startup_outer,
 								  cheapest_total_inner,
+								  inner_unique,
 								  restrictlist,
 								  hashclauses);
 
@@ -1360,6 +1412,7 @@ hash_inner_and_outer(PlannerInfo *root,
 									  extra_lateral_rels,
 									  outerpath,
 									  innerpath,
+									  inner_unique,
 									  restrictlist,
 									  hashclauses);
 				}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index cb69c03..448c556 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -131,12 +131,12 @@ static BitmapAnd *make_bitmap_and(List *bitmapplans);
 static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
-			  Plan *lefttree, Plan *righttree,
+			  Plan *lefttree, Plan *righttree, bool inner_unique,
 			  JoinType jointype);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
-			  Plan *lefttree, Plan *righttree,
+			  Plan *lefttree, Plan *righttree, bool inner_unique,
 			  JoinType jointype);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
@@ -151,7 +151,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   Oid *mergecollations,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
-			   Plan *lefttree, Plan *righttree,
+			   Plan *lefttree, Plan *righttree, bool inner_unique,
 			   JoinType jointype);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
@@ -2192,6 +2192,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
+							  best_path->inner_unique,
 							  best_path->jointype);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->path);
@@ -2486,6 +2487,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
+							   best_path->jpath.inner_unique,
 							   best_path->jpath.jointype);
 
 	/* Costs of sort and material steps are included in path cost already */
@@ -2612,6 +2614,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
+							  best_path->jpath.inner_unique,
 							  best_path->jpath.jointype);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
@@ -3717,6 +3720,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
+			  bool  inner_unique,
 			  JoinType jointype)
 {
 	NestLoop   *node = makeNode(NestLoop);
@@ -3729,6 +3733,7 @@ make_nestloop(List *tlist,
 	plan->righttree = righttree;
 	node->join.jointype = jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = inner_unique;
 	node->nestParams = nestParams;
 
 	return node;
@@ -3741,6 +3746,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
+			  bool  inner_unique,
 			  JoinType jointype)
 {
 	HashJoin   *node = makeNode(HashJoin);
@@ -3754,6 +3760,7 @@ make_hashjoin(List *tlist,
 	node->hashclauses = hashclauses;
 	node->join.jointype = jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = inner_unique;
 
 	return node;
 }
@@ -3801,6 +3808,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
+			   bool inner_unique,
 			   JoinType jointype)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
@@ -3818,6 +3826,7 @@ make_mergejoin(List *tlist,
 	node->mergeNullsFirst = mergenullsfirst;
 	node->join.jointype = jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = inner_unique;
 
 	return node;
 }
@@ -4586,7 +4595,6 @@ make_unique(Plan *lefttree, List *distinctList)
 	node->numCols = numCols;
 	node->uniqColIdx = uniqColIdx;
 	node->uniqOperators = uniqOperators;
-
 	return node;
 }
 
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index faca30b..299a51d 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1135,8 +1135,8 @@ create_unique_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
 	 */
 	if (rel->rtekind == RTE_RELATION && sjinfo->semi_can_btree &&
 		relation_has_unique_index_for(root, rel, NIL,
-									  sjinfo->semi_rhs_exprs,
-									  sjinfo->semi_operators))
+									   sjinfo->semi_rhs_exprs,
+									   sjinfo->semi_operators))
 	{
 		pathnode->umethod = UNIQUE_PATH_NOOP;
 		pathnode->path.rows = rel->rows;
@@ -1534,6 +1534,7 @@ create_nestloop_path(PlannerInfo *root,
 					 SemiAntiJoinFactors *semifactors,
 					 Path *outer_path,
 					 Path *inner_path,
+                     bool  inner_unique,
 					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
@@ -1581,6 +1582,7 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->jointype = jointype;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
+	pathnode->inner_unique = inner_unique;
 	pathnode->joinrestrictinfo = restrict_clauses;
 
 	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
@@ -1615,6 +1617,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
 					  Path *inner_path,
+					  bool  inner_unique,
 					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
@@ -1638,6 +1641,7 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.inner_unique = inner_unique;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
@@ -1674,6 +1678,7 @@ create_hashjoin_path(PlannerInfo *root,
 					 SemiAntiJoinFactors *semifactors,
 					 Path *outer_path,
 					 Path *inner_path,
+					 bool  inner_unique,
 					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
@@ -1706,6 +1711,7 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.inner_unique = inner_unique;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 59b17f3..f86f806 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1562,6 +1562,7 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		inner_unique;
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 21cbfa8..122f2f4 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -543,6 +543,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 334cf51..c1ebfdb 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1030,6 +1030,7 @@ typedef struct JoinPath
 
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
+	bool		inner_unique;
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 9923f0e..cefcecc 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -94,6 +94,7 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 SemiAntiJoinFactors *semifactors,
 					 Path *outer_path,
 					 Path *inner_path,
+					 bool  inner_unique,
 					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
@@ -105,6 +106,7 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
 					  Path *inner_path,
+					  bool  inner_unique,
 					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
@@ -120,6 +122,7 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 SemiAntiJoinFactors *semifactors,
 					 Path *outer_path,
 					 Path *inner_path,
+					 bool  inner_unique,
 					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index dfae84e..ad1d673 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -186,7 +186,7 @@ explain (costs off)
   select * from ec1, ec2 where ff = x1 and x1 = '42'::int8alias2;
                QUERY PLAN                
 -----------------------------------------
- Nested Loop
+ Nested Loop(inner unique)
    ->  Seq Scan on ec2
          Filter: (x1 = '42'::int8alias2)
    ->  Index Scan using ec1_pkey on ec1
@@ -310,7 +310,7 @@ explain (costs off)
          ->  Index Scan using ec1_expr3 on ec1 ec1_5
          ->  Index Scan using ec1_expr4 on ec1 ec1_6
    ->  Materialize
-         ->  Merge Join
+         ->  Merge Join(inner unique)
                Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
                ->  Merge Append
                      Sort Key: (((ec1_1.ff + 2) + 1))
@@ -365,7 +365,7 @@ explain (costs off)
   where ss1.x = ec1.f1 and ec1.ff = 42::int8;
                      QUERY PLAN                      
 -----------------------------------------------------
- Merge Join
+ Merge Join(inner unique)
    Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
    ->  Merge Append
          Sort Key: (((ec1_1.ff + 2) + 1))
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 57fc910..b6e3024 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2614,8 +2614,8 @@ from nt3 as nt3
 where nt3.id = 1 and ss2.b3;
                   QUERY PLAN                   
 -----------------------------------------------
- Nested Loop
-   ->  Nested Loop
+ Nested Loop(inner unique)
+   ->  Nested Loop(inner unique)
          ->  Index Scan using nt3_pkey on nt3
                Index Cond: (id = 1)
          ->  Index Scan using nt2_pkey on nt2
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index f41bef1..aaf585d 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -248,7 +248,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle);
 EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
                      QUERY PLAN                     
 ----------------------------------------------------
- Nested Loop
+ Nested Loop(inner unique)
    ->  Subquery Scan on document
          Filter: f_leak(document.dtitle)
          ->  Seq Scan on document document_1
#21Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: Kyotaro HORIGUCHI (#20)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

Hello,

Hello, The attached is non-search version of unique join.

This is a quite bucket-brigade soye makes many functions got
changes to have new parameter only to convey the new bool
information. I know it's very bad.

After some consideration, I removed almost all of such parameters
from path creation function and confine the main work within
add_paths_to_joinrel. After all the patch shrinked and looks
better. Addition to that, somehow, some additional regtests found
to be inner-unique.

I think this satisfies your wish and implemented in non
exhaustive-seearch-in-jointree manner. It still don't have
regressions for itself but I don't see siginificance in adding
it so much...

regards,

--
Kyotaro Horiguchi
NTT Open Source Software Center

Attachments:

0001-Boost-inner-unique-joins.patchtext/x-patch; charset=us-asciiDownload
>From e1d593a6dd3154d7299b4995c4affa50476df15f Mon Sep 17 00:00:00 2001
From: Kyotaro Horiguchi <horiguchi.kyotaro@lab.ntt.co.jp>
Date: Tue, 17 Mar 2015 15:37:47 +0900
Subject: [PATCH] Boost inner-unique joins.

Inner-unique joins is accelerated by omitting to fetch the second
inner rows on execution. By this patch allow executor to do so
by detecting inner-unique joins on join path creation.
---
 src/backend/commands/explain.c               |  7 ++++
 src/backend/executor/nodeHashjoin.c          | 12 ++++---
 src/backend/executor/nodeMergejoin.c         | 13 +++++---
 src/backend/executor/nodeNestloop.c          | 12 ++++---
 src/backend/optimizer/path/joinpath.c        | 48 +++++++++++++++++++++++++++-
 src/backend/optimizer/plan/createplan.c      | 28 ++++++++--------
 src/include/nodes/execnodes.h                |  1 +
 src/include/nodes/plannodes.h                |  1 +
 src/include/nodes/relation.h                 |  1 +
 src/test/regress/expected/equivclass.out     |  6 ++--
 src/test/regress/expected/join.out           |  4 +--
 src/test/regress/expected/rowsecurity.out    |  2 +-
 src/test/regress/expected/select_views_1.out |  8 ++---
 13 files changed, 107 insertions(+), 36 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a951c55..b8a68b5 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1151,9 +1151,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
 						appendStringInfo(es->str, " %s Join", jointype);
 					else if (!IsA(plan, NestLoop))
 						appendStringInfoString(es->str, " Join");
+					if (((Join *)plan)->inner_unique)
+						appendStringInfoString(es->str, "(inner unique)");
+
 				}
 				else
+				{
 					ExplainPropertyText("Join Type", jointype, es);
+					ExplainPropertyText("Inner unique", 
+							((Join *)plan)->inner_unique?"true":"false", es);
+				}
 			}
 			break;
 		case T_SetOp:
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 1d78cdf..d3b14e5 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,11 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.inner_unique)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -451,6 +452,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate = makeNode(HashJoinState);
 	hjstate->js.ps.plan = (Plan *) node;
 	hjstate->js.ps.state = estate;
+	hjstate->js.inner_unique = node->join.inner_unique;
 
 	/*
 	 * Miscellaneous initialization
@@ -498,8 +500,10 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			hjstate->js.inner_unique = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 15742c5..3c21ffe 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,11 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.inner_unique)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1486,6 +1487,8 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.plan = (Plan *) node;
 	mergestate->js.ps.state = estate;
 
+	mergestate->js.inner_unique = node->join.inner_unique;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -1553,8 +1556,10 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			mergestate->js.inner_unique = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e66bcda..342c448 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * We'll consider returning the first match if the inner is
+			 * unique, but after that we're done with this outer tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.inner_unique)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -310,6 +310,8 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.plan = (Plan *) node;
 	nlstate->js.ps.state = estate;
 
+	nlstate->js.inner_unique = node->join.inner_unique;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -354,8 +356,10 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			nlstate->js.inner_unique = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 1da953f..52e8693 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -49,7 +49,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+										   RelOptInfo *innerrel);
 
 /*
  * add_paths_to_joinrel
@@ -260,6 +261,51 @@ add_paths_to_joinrel(PlannerInfo *root,
 							 restrictlist, jointype,
 							 sjinfo, &semifactors,
 							 param_source_rels, extra_lateral_rels);
+
+	/*
+	 * We can optimize inner loop execution for joins on which the inner rel
+	 * is unique on the restrictlist.
+	 */
+	if (jointype == JOIN_INNER &&
+ 		innerrel->rtekind == RTE_RELATION &&
+		restrictlist)
+	{
+		/* relation_has_unique_index_for may add some restrictions */
+		int org_len = list_length(restrictlist);
+		ListCell *lc;
+
+		/*
+		 * relation_has_unique_index_for requires restrict infos to have
+		 * correct clause direction info
+		 */
+		foreach (lc, restrictlist)
+		{
+			clause_sides_match_join((RestrictInfo *) lfirst(lc),
+									outerrel, innerrel);
+		}
+		if (relation_has_unique_index_for(root, innerrel, restrictlist,
+										  NIL, NIL))
+		{
+			/*
+			 * OK, this join has the unique inner rel, so mark the paths added
+			 * now that the inner is unique
+			 */
+			foreach (lc, joinrel->pathlist)
+			{
+				JoinPath *jp = (JoinPath *) lfirst(lc);
+
+				/*
+				 * This relies on that add_paths_to_joinrel won't be called
+				 * with same outer/inner rels for dirrerent restrictlist.
+				 */
+				if (jp->outerjoinpath->parent == outerrel &&
+					jp->innerjoinpath->parent == innerrel)
+					jp->inner_unique = true;
+			}
+		}
+		/* Remove restirictions added by the function */
+		list_truncate(restrictlist, org_len);
+	}
 }
 
 /*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index cb69c03..ea08695 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -131,13 +131,12 @@ static BitmapAnd *make_bitmap_and(List *bitmapplans);
 static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
-			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  Plan *lefttree, Plan *righttree, JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -152,7 +151,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2192,7 +2191,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->path);
 
@@ -2486,7 +2485,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
@@ -2612,7 +2611,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3717,7 +3716,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3727,8 +3726,9 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 	node->nestParams = nestParams;
 
 	return node;
@@ -3741,7 +3741,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3752,8 +3752,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -3801,7 +3802,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3816,8 +3817,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 59b17f3..f86f806 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1562,6 +1562,7 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		inner_unique;
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 21cbfa8..122f2f4 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -543,6 +543,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 334cf51..c1ebfdb 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1030,6 +1030,7 @@ typedef struct JoinPath
 
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
+	bool		inner_unique;
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index dfae84e..ad1d673 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -186,7 +186,7 @@ explain (costs off)
   select * from ec1, ec2 where ff = x1 and x1 = '42'::int8alias2;
                QUERY PLAN                
 -----------------------------------------
- Nested Loop
+ Nested Loop(inner unique)
    ->  Seq Scan on ec2
          Filter: (x1 = '42'::int8alias2)
    ->  Index Scan using ec1_pkey on ec1
@@ -310,7 +310,7 @@ explain (costs off)
          ->  Index Scan using ec1_expr3 on ec1 ec1_5
          ->  Index Scan using ec1_expr4 on ec1 ec1_6
    ->  Materialize
-         ->  Merge Join
+         ->  Merge Join(inner unique)
                Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
                ->  Merge Append
                      Sort Key: (((ec1_1.ff + 2) + 1))
@@ -365,7 +365,7 @@ explain (costs off)
   where ss1.x = ec1.f1 and ec1.ff = 42::int8;
                      QUERY PLAN                      
 -----------------------------------------------------
- Merge Join
+ Merge Join(inner unique)
    Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
    ->  Merge Append
          Sort Key: (((ec1_1.ff + 2) + 1))
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 57fc910..b6e3024 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2614,8 +2614,8 @@ from nt3 as nt3
 where nt3.id = 1 and ss2.b3;
                   QUERY PLAN                   
 -----------------------------------------------
- Nested Loop
-   ->  Nested Loop
+ Nested Loop(inner unique)
+   ->  Nested Loop(inner unique)
          ->  Index Scan using nt3_pkey on nt3
                Index Cond: (id = 1)
          ->  Index Scan using nt2_pkey on nt2
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index f41bef1..aaf585d 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -248,7 +248,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle);
 EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
                      QUERY PLAN                     
 ----------------------------------------------------
- Nested Loop
+ Nested Loop(inner unique)
    ->  Subquery Scan on document
          Filter: f_leak(document.dtitle)
          ->  Seq Scan on document document_1
diff --git a/src/test/regress/expected/select_views_1.out b/src/test/regress/expected/select_views_1.out
index ce22bfa..a37bde4 100644
--- a/src/test/regress/expected/select_views_1.out
+++ b/src/test/regress/expected/select_views_1.out
@@ -1365,7 +1365,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Join(inner unique)
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1386,7 +1386,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Join(inner unique)
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1420,7 +1420,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1451,7 +1451,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
-- 
2.1.0.GIT

#22David Rowley
dgrowleyml@gmail.com
In reply to: Kyotaro HORIGUCHI (#21)
Re: Performance improvement for joins where outer side is unique

On 20 March 2015 at 16:11, Kyotaro HORIGUCHI <
horiguchi.kyotaro@lab.ntt.co.jp> wrote:

I think this satisfies your wish and implemented in non
exhaustive-seearch-in-jointree manner. It still don't have
regressions for itself but I don't see siginificance in adding
it so much...

This seems quite a bit better. Having the inner_unique variable as part of
the JoinPath struct seems much better than what I had. This seems to remove
the requirement of my patch that all joins to that RelOptInfo be unique.

I also quite like the more recent change to make_hashjoin and co. just pass
the JoinPath as a parameter.

I don't really like the "(inner unique)" being tagged onto the end of the
join node, but there's not much point in spending too much time talking
about that right now. There's much better things to talk about. I'm sure we
can all bikeshed around that one later.

In joinpath.c you have a restriction to only perform the unique check for
inner joins.. This should work ok for left joins too, but it would probably
be more efficient to have the left join removal code analyse the
SpecialJoinInfos during checks for left join removals. I think it would
just be a matter of breaking down the join removal code similar to how I
did in my patch, but this time add a bool inner_unique to the
SpecialJoinInfo struct. The join_is_legal() function seems to select the
correct SpecialJoinInfo if one exists, so add_paths_to_joinrel() shouldn't
need to call relation_has_unique_index_for() if it's a LEFT JOIN, as we'll
already know if it's unique by just looking at the property.

You've also lost the ability to detect that subqueries are unique:

create table j1(id int primary key);
create table j2(value int not null);
explain select * from j1 inner join (select distinct value from j2) j2 on
j1.id=j2.value;

The left join removal code properly detects this, so I think unique joins
should too.

I can continue working on your patch if you like? Or are you planning to go
further with it?

Regards
David Rowley

#23David Rowley
dgrowleyml@gmail.com
In reply to: David Rowley (#22)
2 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 20 March 2015 at 21:11, David Rowley <dgrowleyml@gmail.com> wrote:

I can continue working on your patch if you like? Or are you planning to
go further with it?

I've been working on this more over the weekend and I've re-factored things
to allow LEFT JOINs to be properly marked as unique.
I've also made changes to re-add support for detecting the uniqueness of
sub-queries.

Also, I've added modified the costing for hash and nested loop joins to
reduce the cost for unique inner joins to cost the join the same as it does
for SEMI joins. This has tipped the scales on a few plans in the regression
tests.

Also, please see attached unijoin_analysis.patch. This just adds some code
which spouts out notices when join nodes are initialised which states if
the join is unique or not. Running the regression tests with this patch in
places gives:

Unique Inner: Yes == 753 hits
Unique Inner: No == 1430 hits

So it seems we can increase the speed of about 1 third of joins by about
10%.
A quick scan of the "No"s seems to show quite a few cases which do not look
that real world like. e.g cartesian join.

It would be great if someone could run some PostgreSQL application with
these 2 patches applied, and then grep the logs for the Unique Inner
results... Just to get a better idea of how many joins in a real world case
will benefit from this patch.

Regards

David Rowley

Attachments:

unijoin_2015-03-22_87bc41e.patchapplication/octet-stream; name=unijoin_2015-03-22_87bc41e.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a951c55..ae69fc1 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1151,9 +1151,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
 						appendStringInfo(es->str, " %s Join", jointype);
 					else if (!IsA(plan, NestLoop))
 						appendStringInfoString(es->str, " Join");
+					if (((Join *)plan)->inner_unique)
+						appendStringInfoString(es->str, "(inner unique)");
+
 				}
 				else
+				{
 					ExplainPropertyText("Join Type", jointype, es);
+					ExplainPropertyText("Inner unique",
+							((Join *)plan)->inner_unique?"true":"false", es);
+				}
 			}
 			break;
 		case T_SetOp:
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 1d78cdf..f6cd8e1 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple. For the case of SEMI joins, we want to skip to
+					 * the next outer row after having matched 1 inner row.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.inner_unique)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -451,6 +453,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate = makeNode(HashJoinState);
 	hjstate->js.ps.plan = (Plan *) node;
 	hjstate->js.ps.state = estate;
+	hjstate->js.inner_unique = node->join.inner_unique;
 
 	/*
 	 * Miscellaneous initialization
@@ -498,8 +501,10 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			hjstate->js.inner_unique = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 15742c5..28dfbbe 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,12 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple. For the case of SEMI joins, we want to skip to
+					 * the next outer row after having matched 1 inner row.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.inner_unique)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1486,6 +1488,8 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.plan = (Plan *) node;
 	mergestate->js.ps.state = estate;
 
+	mergestate->js.inner_unique = node->join.inner_unique;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -1553,8 +1557,10 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			mergestate->js.inner_unique = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e66bcda..8cf0e71 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,12 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * We'll consider returning the first match if the inner is
+			 * unique, but after that we're done with this outer tuple.
+			 * For the case of SEMI joins, we want to skip to the next outer
+			 * row after having matched 1 inner row.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.inner_unique)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -310,6 +312,8 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.plan = (Plan *) node;
 	nlstate->js.ps.state = estate;
 
+	nlstate->js.inner_unique = node->join.inner_unique;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -354,8 +358,10 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			nlstate->js.inner_unique = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 029761e..4008328 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1944,6 +1944,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(is_unique_join);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 190e50a..25885df 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -798,6 +798,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(is_unique_join);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 385b289..69e7353 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1948,6 +1948,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(is_unique_join);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 1a0d358..30d5c80 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1746,7 +1746,9 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI ||
+		sjinfo->is_unique_join)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
@@ -2658,7 +2660,9 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		sjinfo->is_unique_join)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 1da953f..b0bc3f6 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -18,6 +18,7 @@
 
 #include "executor/executor.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -49,7 +50,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+										   RelOptInfo *innerrel);
 
 /*
  * add_paths_to_joinrel
@@ -260,8 +262,74 @@ add_paths_to_joinrel(PlannerInfo *root,
 							 restrictlist, jointype,
 							 sjinfo, &semifactors,
 							 param_source_rels, extra_lateral_rels);
+
+	if (restrictlist == NIL)
+		return;
+
+	/*
+	 * We can optimize inner loop execution for joins on which the inner rel
+	 * is unique on the restrictlist.
+	 */
+	if (jointype == JOIN_INNER)
+	{
+		/*
+		 * remember the number of items that were in the restrictlist as
+		 * the call to relation_has_unique_index_for may add more items
+		 * which we'll need to remove later.
+		 */
+		int org_len = list_length(restrictlist);
+
+		/*
+		 * rel_is_distinct_for requires restrict infos to have the
+		 * correct clause direction info
+		 */
+		foreach(lc, restrictlist)
+		{
+			clause_sides_match_join((RestrictInfo *)lfirst(lc),
+				outerrel, innerrel);
+		}
+
+		sjinfo->is_unique_join = rel_is_distinct_for(root, innerrel, restrictlist);
+
+		/* Remove any list items added by rel_is_distinct_for */
+		list_truncate(restrictlist, org_len);
+	}
+
+	/*
+	 * left joins were already checked for uniqueness in analyzejoins.c
+	 */
+
+	if (sjinfo->is_unique_join)
+	{
+		/*
+		 * OK, this join has the unique inner rel, so mark the paths added
+		 * now that the inner is unique
+		 */
+		foreach(lc, joinrel->pathlist)
+		{
+			JoinPath *jp = (JoinPath *)lfirst(lc);
+
+			/*
+			 * This relies on that add_paths_to_joinrel won't be called
+			 * with same outer/inner rels for different restrictlist.
+			 */
+			switch (jp->jointype)
+			{
+				case JOIN_INNER:
+				case JOIN_LEFT:
+					if (jp->outerjoinpath->parent == outerrel &&
+						jp->innerjoinpath->parent == innerrel)
+						jp->inner_unique = true;
+					break;
+				default:
+					break;
+			}
+		}
+	}
+
 }
 
+
 /*
  * try_nestloop_path
  *	  Consider a nestloop join path; if it appears useful, push it into
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index fe9fd57..a79c194 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -624,6 +624,7 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 		/* we don't bother trying to make the remaining fields valid */
 		sjinfo->lhs_strict = false;
 		sjinfo->delay_upper_joins = false;
+		sjinfo->is_unique_join = false;
 		sjinfo->semi_can_btree = false;
 		sjinfo->semi_can_hash = false;
 		sjinfo->semi_operators = NIL;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 11d3933..5d0cd2e 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -33,12 +33,28 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
 
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		if (specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->is_unique_join = true;
+	}
+}
+
 /*
  * remove_useless_joins
  *		Check for relations that don't actually need to be joined at all,
@@ -91,6 +107,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to mark some joins as unique which we could
+		 * not do before
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -151,17 +173,17 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must not duplicate its outer side and must be a non-delaying left
+	 * join to a single baserel, else we aren't going to be able to do anything
+	 * with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->is_unique_join ||
+		sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -170,38 +192,7 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -212,7 +203,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -253,6 +245,49 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join will never produce
+ *		more than 1 row per outer row, otherwise returns false if there is
+ *		insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Query	   *subquery = NULL;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* check if we've already marked this join as unique on a previous call */
+	if (sjinfo->is_unique_join)
+		return true;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -274,10 +309,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we can't
+			 * mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -300,71 +333,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -564,6 +535,125 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list, callers should either
+ * make a copy of the list or trim it back to it's original length after
+ * calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *)lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *)rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *)get_rightop(rinfo->clause);
+			else
+				var = (Var *)get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false; /* can't prove rel to be distinct over clause_list */
+}
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's nothing to prove
+		 * uniqueness on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index cb69c03..ea08695 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -131,13 +131,12 @@ static BitmapAnd *make_bitmap_and(List *bitmapplans);
 static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
-			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  Plan *lefttree, Plan *righttree, JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -152,7 +151,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2192,7 +2191,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->path);
 
@@ -2486,7 +2485,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
@@ -2612,7 +2611,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3717,7 +3716,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3727,8 +3726,9 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 	node->nestParams = nestParams;
 
 	return node;
@@ -3741,7 +3741,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3752,8 +3752,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -3801,7 +3802,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3816,8 +3817,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index a7655e4..8094880 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1087,6 +1087,7 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	sjinfo->is_unique_join = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 848df97..55310d8 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -174,6 +174,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Analyze joins to find out which ones have a unique inner side */
+	mark_unique_joins(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 59b17f3..f86f806 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1562,6 +1562,7 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		inner_unique;
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 21cbfa8..122f2f4 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -543,6 +543,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 334cf51..776a269 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1030,6 +1030,7 @@ typedef struct JoinPath
 
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
+	bool		inner_unique;
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
@@ -1406,6 +1407,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index fa72918..7a85227 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -122,7 +122,11 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+								List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index dfae84e..ad1d673 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -186,7 +186,7 @@ explain (costs off)
   select * from ec1, ec2 where ff = x1 and x1 = '42'::int8alias2;
                QUERY PLAN                
 -----------------------------------------
- Nested Loop
+ Nested Loop(inner unique)
    ->  Seq Scan on ec2
          Filter: (x1 = '42'::int8alias2)
    ->  Index Scan using ec1_pkey on ec1
@@ -310,7 +310,7 @@ explain (costs off)
          ->  Index Scan using ec1_expr3 on ec1 ec1_5
          ->  Index Scan using ec1_expr4 on ec1 ec1_6
    ->  Materialize
-         ->  Merge Join
+         ->  Merge Join(inner unique)
                Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
                ->  Merge Append
                      Sort Key: (((ec1_1.ff + 2) + 1))
@@ -365,7 +365,7 @@ explain (costs off)
   where ss1.x = ec1.f1 and ec1.ff = 42::int8;
                      QUERY PLAN                      
 -----------------------------------------------------
- Merge Join
+ Merge Join(inner unique)
    Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
    ->  Merge Append
          Sort Key: (((ec1_1.ff + 2) + 1))
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 56e2c99..e3fe97f 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -34,119 +34,119 @@ INSERT INTO d(aa) VALUES('dddddddd');
 SELECT relname, a.* FROM a, pg_class where a.tableoid = pg_class.oid;
  relname |    aa    
 ---------+----------
- a       | aaa
- a       | aaaa
- a       | aaaaa
- a       | aaaaaa
- a       | aaaaaaa
  a       | aaaaaaaa
- b       | bbb
- b       | bbbb
- b       | bbbbb
- b       | bbbbbb
- b       | bbbbbbb
+ a       | aaaaaaa
+ a       | aaaaaa
+ a       | aaaaa
+ a       | aaaa
+ a       | aaa
  b       | bbbbbbbb
- c       | ccc
- c       | cccc
- c       | ccccc
- c       | cccccc
- c       | ccccccc
+ b       | bbbbbbb
+ b       | bbbbbb
+ b       | bbbbb
+ b       | bbbb
+ b       | bbb
  c       | cccccccc
- d       | ddd
- d       | dddd
- d       | ddddd
- d       | dddddd
- d       | ddddddd
+ c       | ccccccc
+ c       | cccccc
+ c       | ccccc
+ c       | cccc
+ c       | ccc
  d       | dddddddd
+ d       | ddddddd
+ d       | dddddd
+ d       | ddddd
+ d       | dddd
+ d       | ddd
 (24 rows)
 
 SELECT relname, b.* FROM b, pg_class where b.tableoid = pg_class.oid;
  relname |    aa    | bb 
 ---------+----------+----
- b       | bbb      | 
- b       | bbbb     | 
- b       | bbbbb    | 
- b       | bbbbbb   | 
- b       | bbbbbbb  | 
  b       | bbbbbbbb | 
- d       | ddd      | 
- d       | dddd     | 
- d       | ddddd    | 
- d       | dddddd   | 
- d       | ddddddd  | 
+ b       | bbbbbbb  | 
+ b       | bbbbbb   | 
+ b       | bbbbb    | 
+ b       | bbbb     | 
+ b       | bbb      | 
  d       | dddddddd | 
+ d       | ddddddd  | 
+ d       | dddddd   | 
+ d       | ddddd    | 
+ d       | dddd     | 
+ d       | ddd      | 
 (12 rows)
 
 SELECT relname, c.* FROM c, pg_class where c.tableoid = pg_class.oid;
  relname |    aa    | cc 
 ---------+----------+----
- c       | ccc      | 
- c       | cccc     | 
- c       | ccccc    | 
- c       | cccccc   | 
- c       | ccccccc  | 
  c       | cccccccc | 
- d       | ddd      | 
- d       | dddd     | 
- d       | ddddd    | 
- d       | dddddd   | 
- d       | ddddddd  | 
+ c       | ccccccc  | 
+ c       | cccccc   | 
+ c       | ccccc    | 
+ c       | cccc     | 
+ c       | ccc      | 
  d       | dddddddd | 
+ d       | ddddddd  | 
+ d       | dddddd   | 
+ d       | ddddd    | 
+ d       | dddd     | 
+ d       | ddd      | 
 (12 rows)
 
 SELECT relname, d.* FROM d, pg_class where d.tableoid = pg_class.oid;
  relname |    aa    | bb | cc | dd 
 ---------+----------+----+----+----
- d       | ddd      |    |    | 
- d       | dddd     |    |    | 
- d       | ddddd    |    |    | 
- d       | dddddd   |    |    | 
- d       | ddddddd  |    |    | 
  d       | dddddddd |    |    | 
+ d       | ddddddd  |    |    | 
+ d       | dddddd   |    |    | 
+ d       | ddddd    |    |    | 
+ d       | dddd     |    |    | 
+ d       | ddd      |    |    | 
 (6 rows)
 
 SELECT relname, a.* FROM ONLY a, pg_class where a.tableoid = pg_class.oid;
  relname |    aa    
 ---------+----------
- a       | aaa
- a       | aaaa
- a       | aaaaa
- a       | aaaaaa
- a       | aaaaaaa
  a       | aaaaaaaa
+ a       | aaaaaaa
+ a       | aaaaaa
+ a       | aaaaa
+ a       | aaaa
+ a       | aaa
 (6 rows)
 
 SELECT relname, b.* FROM ONLY b, pg_class where b.tableoid = pg_class.oid;
  relname |    aa    | bb 
 ---------+----------+----
- b       | bbb      | 
- b       | bbbb     | 
- b       | bbbbb    | 
- b       | bbbbbb   | 
- b       | bbbbbbb  | 
  b       | bbbbbbbb | 
+ b       | bbbbbbb  | 
+ b       | bbbbbb   | 
+ b       | bbbbb    | 
+ b       | bbbb     | 
+ b       | bbb      | 
 (6 rows)
 
 SELECT relname, c.* FROM ONLY c, pg_class where c.tableoid = pg_class.oid;
  relname |    aa    | cc 
 ---------+----------+----
- c       | ccc      | 
- c       | cccc     | 
- c       | ccccc    | 
- c       | cccccc   | 
- c       | ccccccc  | 
  c       | cccccccc | 
+ c       | ccccccc  | 
+ c       | cccccc   | 
+ c       | ccccc    | 
+ c       | cccc     | 
+ c       | ccc      | 
 (6 rows)
 
 SELECT relname, d.* FROM ONLY d, pg_class where d.tableoid = pg_class.oid;
  relname |    aa    | bb | cc | dd 
 ---------+----------+----+----+----
- d       | ddd      |    |    | 
- d       | dddd     |    |    | 
- d       | ddddd    |    |    | 
- d       | dddddd   |    |    | 
- d       | ddddddd  |    |    | 
  d       | dddddddd |    |    | 
+ d       | ddddddd  |    |    | 
+ d       | dddddd   |    |    | 
+ d       | ddddd    |    |    | 
+ d       | dddd     |    |    | 
+ d       | ddd      |    |    | 
 (6 rows)
 
 UPDATE a SET aa='zzzz' WHERE aa='aaaa';
@@ -157,143 +157,143 @@ UPDATE a SET aa='zzzzzz' WHERE aa LIKE 'aaa%';
 SELECT relname, a.* FROM a, pg_class where a.tableoid = pg_class.oid;
  relname |    aa    
 ---------+----------
- a       | zzzz
- a       | zzzzz
  a       | zzzzzz
  a       | zzzzzz
  a       | zzzzzz
  a       | zzzzzz
- b       | bbb
- b       | bbbb
- b       | bbbbb
- b       | bbbbbb
- b       | bbbbbbb
+ a       | zzzzz
+ a       | zzzz
  b       | bbbbbbbb
- c       | ccc
- c       | cccc
- c       | ccccc
- c       | cccccc
- c       | ccccccc
+ b       | bbbbbbb
+ b       | bbbbbb
+ b       | bbbbb
+ b       | bbbb
+ b       | bbb
  c       | cccccccc
- d       | ddd
- d       | dddd
- d       | ddddd
- d       | dddddd
- d       | ddddddd
+ c       | ccccccc
+ c       | cccccc
+ c       | ccccc
+ c       | cccc
+ c       | ccc
  d       | dddddddd
+ d       | ddddddd
+ d       | dddddd
+ d       | ddddd
+ d       | dddd
+ d       | ddd
 (24 rows)
 
 SELECT relname, b.* FROM b, pg_class where b.tableoid = pg_class.oid;
  relname |    aa    | bb 
 ---------+----------+----
- b       | bbb      | 
- b       | bbbb     | 
- b       | bbbbb    | 
- b       | bbbbbb   | 
- b       | bbbbbbb  | 
  b       | bbbbbbbb | 
- d       | ddd      | 
- d       | dddd     | 
- d       | ddddd    | 
- d       | dddddd   | 
- d       | ddddddd  | 
+ b       | bbbbbbb  | 
+ b       | bbbbbb   | 
+ b       | bbbbb    | 
+ b       | bbbb     | 
+ b       | bbb      | 
  d       | dddddddd | 
+ d       | ddddddd  | 
+ d       | dddddd   | 
+ d       | ddddd    | 
+ d       | dddd     | 
+ d       | ddd      | 
 (12 rows)
 
 SELECT relname, c.* FROM c, pg_class where c.tableoid = pg_class.oid;
  relname |    aa    | cc 
 ---------+----------+----
- c       | ccc      | 
- c       | cccc     | 
- c       | ccccc    | 
- c       | cccccc   | 
- c       | ccccccc  | 
  c       | cccccccc | 
- d       | ddd      | 
- d       | dddd     | 
- d       | ddddd    | 
- d       | dddddd   | 
- d       | ddddddd  | 
+ c       | ccccccc  | 
+ c       | cccccc   | 
+ c       | ccccc    | 
+ c       | cccc     | 
+ c       | ccc      | 
  d       | dddddddd | 
+ d       | ddddddd  | 
+ d       | dddddd   | 
+ d       | ddddd    | 
+ d       | dddd     | 
+ d       | ddd      | 
 (12 rows)
 
 SELECT relname, d.* FROM d, pg_class where d.tableoid = pg_class.oid;
  relname |    aa    | bb | cc | dd 
 ---------+----------+----+----+----
- d       | ddd      |    |    | 
- d       | dddd     |    |    | 
- d       | ddddd    |    |    | 
- d       | dddddd   |    |    | 
- d       | ddddddd  |    |    | 
  d       | dddddddd |    |    | 
+ d       | ddddddd  |    |    | 
+ d       | dddddd   |    |    | 
+ d       | ddddd    |    |    | 
+ d       | dddd     |    |    | 
+ d       | ddd      |    |    | 
 (6 rows)
 
 SELECT relname, a.* FROM ONLY a, pg_class where a.tableoid = pg_class.oid;
  relname |   aa   
 ---------+--------
- a       | zzzz
- a       | zzzzz
  a       | zzzzzz
  a       | zzzzzz
  a       | zzzzzz
  a       | zzzzzz
+ a       | zzzzz
+ a       | zzzz
 (6 rows)
 
 SELECT relname, b.* FROM ONLY b, pg_class where b.tableoid = pg_class.oid;
  relname |    aa    | bb 
 ---------+----------+----
- b       | bbb      | 
- b       | bbbb     | 
- b       | bbbbb    | 
- b       | bbbbbb   | 
- b       | bbbbbbb  | 
  b       | bbbbbbbb | 
+ b       | bbbbbbb  | 
+ b       | bbbbbb   | 
+ b       | bbbbb    | 
+ b       | bbbb     | 
+ b       | bbb      | 
 (6 rows)
 
 SELECT relname, c.* FROM ONLY c, pg_class where c.tableoid = pg_class.oid;
  relname |    aa    | cc 
 ---------+----------+----
- c       | ccc      | 
- c       | cccc     | 
- c       | ccccc    | 
- c       | cccccc   | 
- c       | ccccccc  | 
  c       | cccccccc | 
+ c       | ccccccc  | 
+ c       | cccccc   | 
+ c       | ccccc    | 
+ c       | cccc     | 
+ c       | ccc      | 
 (6 rows)
 
 SELECT relname, d.* FROM ONLY d, pg_class where d.tableoid = pg_class.oid;
  relname |    aa    | bb | cc | dd 
 ---------+----------+----+----+----
- d       | ddd      |    |    | 
- d       | dddd     |    |    | 
- d       | ddddd    |    |    | 
- d       | dddddd   |    |    | 
- d       | ddddddd  |    |    | 
  d       | dddddddd |    |    | 
+ d       | ddddddd  |    |    | 
+ d       | dddddd   |    |    | 
+ d       | ddddd    |    |    | 
+ d       | dddd     |    |    | 
+ d       | ddd      |    |    | 
 (6 rows)
 
 UPDATE b SET aa='new';
 SELECT relname, a.* FROM a, pg_class where a.tableoid = pg_class.oid;
  relname |    aa    
 ---------+----------
- a       | zzzz
- a       | zzzzz
  a       | zzzzzz
  a       | zzzzzz
  a       | zzzzzz
  a       | zzzzzz
+ a       | zzzzz
+ a       | zzzz
  b       | new
  b       | new
  b       | new
  b       | new
  b       | new
  b       | new
- c       | ccc
- c       | cccc
- c       | ccccc
- c       | cccccc
- c       | ccccccc
  c       | cccccccc
+ c       | ccccccc
+ c       | cccccc
+ c       | ccccc
+ c       | cccc
+ c       | ccc
  d       | new
  d       | new
  d       | new
@@ -322,12 +322,12 @@ SELECT relname, b.* FROM b, pg_class where b.tableoid = pg_class.oid;
 SELECT relname, c.* FROM c, pg_class where c.tableoid = pg_class.oid;
  relname |    aa    | cc 
 ---------+----------+----
- c       | ccc      | 
- c       | cccc     | 
- c       | ccccc    | 
- c       | cccccc   | 
- c       | ccccccc  | 
  c       | cccccccc | 
+ c       | ccccccc  | 
+ c       | cccccc   | 
+ c       | ccccc    | 
+ c       | cccc     | 
+ c       | ccc      | 
  d       | new      | 
  d       | new      | 
  d       | new      | 
@@ -350,12 +350,12 @@ SELECT relname, d.* FROM d, pg_class where d.tableoid = pg_class.oid;
 SELECT relname, a.* FROM ONLY a, pg_class where a.tableoid = pg_class.oid;
  relname |   aa   
 ---------+--------
- a       | zzzz
- a       | zzzzz
  a       | zzzzzz
  a       | zzzzzz
  a       | zzzzzz
  a       | zzzzzz
+ a       | zzzzz
+ a       | zzzz
 (6 rows)
 
 SELECT relname, b.* FROM ONLY b, pg_class where b.tableoid = pg_class.oid;
@@ -372,12 +372,12 @@ SELECT relname, b.* FROM ONLY b, pg_class where b.tableoid = pg_class.oid;
 SELECT relname, c.* FROM ONLY c, pg_class where c.tableoid = pg_class.oid;
  relname |    aa    | cc 
 ---------+----------+----
- c       | ccc      | 
- c       | cccc     | 
- c       | ccccc    | 
- c       | cccccc   | 
- c       | ccccccc  | 
  c       | cccccccc | 
+ c       | ccccccc  | 
+ c       | cccccc   | 
+ c       | ccccc    | 
+ c       | cccc     | 
+ c       | ccc      | 
 (6 rows)
 
 SELECT relname, d.* FROM ONLY d, pg_class where d.tableoid = pg_class.oid;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 57fc910..1bfb9a3 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2614,8 +2614,8 @@ from nt3 as nt3
 where nt3.id = 1 and ss2.b3;
                   QUERY PLAN                   
 -----------------------------------------------
- Nested Loop
-   ->  Nested Loop
+ Nested Loop(inner unique)
+   ->  Nested Loop(inner unique)
          ->  Index Scan using nt3_pkey on nt3
                Index Cond: (id = 1)
          ->  Index Scan using nt2_pkey on nt2
@@ -3338,7 +3338,7 @@ explain (costs off)
     on (p.k = ss.k);
            QUERY PLAN            
 ---------------------------------
- Hash Left Join
+ Hash Left Join(inner unique)
    Hash Cond: (p.k = c.k)
    ->  Seq Scan on parent p
    ->  Hash
@@ -4416,3 +4416,239 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join(inner unique)
+   Output: j1.id, j2.id
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(9 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join(inner unique)
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join(inner unique)
+   Output: j1.id, j2.id
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(8 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join(inner unique)
+   Output: j1.id
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop(inner unique)
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  HashAggregate
+         Output: j3.id
+         Group Key: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(10 rows)
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop(inner unique)
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  HashAggregate
+         Output: j3.id
+         Group Key: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(10 rows)
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+          QUERY PLAN          
+------------------------------
+ Nested Loop(inner unique)
+   Output: j1.id, (1)
+   Join Filter: (j1.id = (1))
+   ->  Result
+         Output: 1
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(7 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(7 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop(inner unique)
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(7 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index f41bef1..aaf585d 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -248,7 +248,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle);
 EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
                      QUERY PLAN                     
 ----------------------------------------------------
- Nested Loop
+ Nested Loop(inner unique)
    ->  Subquery Scan on document
          Filter: f_leak(document.dtitle)
          ->  Seq Scan on document document_1
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 82d510d..01b8b45 100644
--- a/src/test/regress/expected/select_views.out
+++ b/src/test/regress/expected/select_views.out
@@ -1365,7 +1365,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Join(inner unique)
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1386,7 +1386,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Join(inner unique)
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1420,7 +1420,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1451,7 +1451,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/expected/select_views_1.out b/src/test/regress/expected/select_views_1.out
index ce22bfa..a37bde4 100644
--- a/src/test/regress/expected/select_views_1.out
+++ b/src/test/regress/expected/select_views_1.out
@@ -1365,7 +1365,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Join(inner unique)
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1386,7 +1386,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Join(inner unique)
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1420,7 +1420,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1451,7 +1451,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 06a27ea..9d27d1e 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1307,3 +1307,98 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
unijoin_analysis.patchapplication/octet-stream; name=unijoin_analysis.patchDownload
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index d3b14e5..c302c71 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -453,6 +453,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.plan = (Plan *) node;
 	hjstate->js.ps.state = estate;
 	hjstate->js.inner_unique = node->join.inner_unique;
+	elog(NOTICE, "Unique Inner: %s", node->join.inner_unique ? "Yes" : "No");
 
 	/*
 	 * Miscellaneous initialization
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 3c21ffe..e1bb7e7 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -1489,6 +1489,7 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	mergestate->js.inner_unique = node->join.inner_unique;
 
+	elog(NOTICE, "Unique Inner: %s", node->join.inner_unique ? "Yes" : "No");
 	/*
 	 * Miscellaneous initialization
 	 *
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 342c448..fcd1ef0 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -311,6 +311,7 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	nlstate->js.inner_unique = node->join.inner_unique;
+	elog(NOTICE, "Unique Inner: %s", node->join.inner_unique ? "Yes" : "No");
 
 	/*
 	 * Miscellaneous initialization
#24Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: David Rowley (#23)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

Hi, thanks for the new patch.

I made an additional shrink from your last one. Do you have a
look on the attached?

At Sun, 22 Mar 2015 19:42:21 +1300, David Rowley <dgrowleyml@gmail.com> wrote in <CAApHDvrKwMmTwkXfn4uazYZA9jQL1c7UwBjBtuwFR69rqLVKfA@mail.gmail.com>

On 20 March 2015 at 21:11, David Rowley <dgrowleyml@gmail.com> wrote:

I can continue working on your patch if you like? Or are you planning to
go further with it?

It's fine that you continue to work on this.

# Sorry for the hardly baked patch which had left many things alone:(

I've been working on this more over the weekend and I've re-factored things
to allow LEFT JOINs to be properly marked as unique.
I've also made changes to re-add support for detecting the uniqueness of
sub-queries.

I don't see the point of calling mark_unique_joins for every
iteration on join_info_list in remove_useless_joins.

The loop already iteraltes on whole the join_info_list so
mark_unique_join as an individual function is needless.

Finally, simply marking uniqueness of join in join_is_removable
seems to be enough, inhibiting early bailing out by the checks on
attribute usage and placeholder let it work as expected.

Reducing changes to this extent, I can easily see what is added
to planning computations. It consists of mainly two factors.

- Unique-join chekcs for every candidate inner joins in
add_paths_to_joinrel.

- Uniqueness check of mergejoinable clause in join-removability
check for every left join, some of which would be skipped
by other checks before.

Also, I've added modified the costing for hash and nested loop joins to
reduce the cost for unique inner joins to cost the join the same as it does
for SEMI joins. This has tipped the scales on a few plans in the regression
tests.

I've forgotten it, but quite important.

Also, please see attached unijoin_analysis.patch. This just adds some code
which spouts out notices when join nodes are initialised which states if
the join is unique or not. Running the regression tests with this patch in
places gives:

Unique Inner: Yes == 753 hits
Unique Inner: No == 1430 hits

So it seems we can increase the speed of about 1 third of joins by about
10%.
A quick scan of the "No"s seems to show quite a few cases which do not look
that real world like. e.g cartesian join.

I don't have an idea how many queries in the reality hit this but
I suppose it's not a few.

It would be great if someone could run some PostgreSQL application with
these 2 patches applied, and then grep the logs for the Unique Inner
results... Just to get a better idea of how many joins in a real world case
will benefit from this patch.

Wow. I think the second patch should be DEBUGx, not NOTICE:)

regards,

--
Kyotaro Horiguchi
NTT Open Source Software Center

Attachments:

0001-Removing-individual-phase-to-mark-unique-joins.patchtext/x-patch; charset=us-asciiDownload
>From 659ae5c80ea9e6dc9f6f0a16a43db66b7329a2be Mon Sep 17 00:00:00 2001
From: Kyotaro Horiguchi <horiguchi.kyotaro@lab.ntt.co.jp>
Date: Tue, 24 Mar 2015 20:59:48 +0900
Subject: [PATCH] Removing individual phase to mark unique joins.

mark_unique_joins() makes redundant loops with remove_useless_joins()
over join_info_list. So removed the function and moved the core steps
into join_is_removable().
---
 src/backend/optimizer/path/joinpath.c     |   3 +-
 src/backend/optimizer/plan/analyzejoins.c | 247 +++++++++++-------------------
 src/backend/optimizer/plan/planmain.c     |   3 -
 src/include/nodes/relation.h              |   1 +
 src/include/optimizer/planmain.h          |   1 -
 5 files changed, 96 insertions(+), 159 deletions(-)

diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index b0bc3f6..909b585 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -296,7 +296,8 @@ add_paths_to_joinrel(PlannerInfo *root,
 	}
 
 	/*
-	 * left joins were already checked for uniqueness in analyzejoins.c
+	 * left joins might be already checked for uniqueness in
+	 * remove_useless_joins()
 	 */
 
 	if (sjinfo->is_unique_join)
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 5d0cd2e..990f3b3 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -33,28 +33,12 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
-static bool specialjoin_is_unique_join(PlannerInfo *root,
-					  SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
 
-void
-mark_unique_joins(PlannerInfo *root, List *joinlist)
-{
-	ListCell   *lc;
-
-	foreach(lc, root->join_info_list)
-	{
-		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
-
-		if (specialjoin_is_unique_join(root, sjinfo))
-			sjinfo->is_unique_join = true;
-	}
-}
-
 /*
  * remove_useless_joins
  *		Check for relations that don't actually need to be joined at all,
@@ -107,12 +91,6 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
-		 * We may now be able to mark some joins as unique which we could
-		 * not do before
-		 */
-		mark_unique_joins(root, joinlist);
-
-		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -173,17 +151,18 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
+	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	int			attroff;
+	List	   *clause_list = NIL;
 	ListCell   *l;
+	int			attroff;
+	bool		removable = true;
 
 	/*
-	 * Join must not duplicate its outer side and must be a non-delaying left
-	 * join to a single baserel, else we aren't going to be able to do anything
-	 * with it.
+	 * Must be a non-delaying left join to a single baserel, else we aren't
+	 * going to be able to do anything with it.
 	 */
-	if (!sjinfo->is_unique_join ||
-		sjinfo->jointype != JOIN_LEFT ||
+	if (sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -192,98 +171,38 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	Assert(innerrel->reloptkind == RELOPT_BASEREL);
-
-	/* Compute the relid set for the join we are considering */
-	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
 
 	/*
-	 * We can't remove the join if any inner-rel attributes are used above the
-	 * join.
-	 *
-	 * Note that this test only detects use of inner-rel attributes in higher
-	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too, but in this case the join
-	 * would not have been marked as unique.
-	 *
-	 * As a micro-optimization, it seems better to start with max_attr and
-	 * count down rather than starting with min_attr and counting up, on the
-	 * theory that the system attributes are somewhat less likely to be wanted
-	 * and should be tested last.
+	 * Before we go to the effort of checking whether any innerrel variables
+	 * are needed above the join, make a quick check to eliminate cases in
+	 * which we will surely be unable to prove uniqueness of the innerrel.
 	 */
-	for (attroff = innerrel->max_attr - innerrel->min_attr;
-		 attroff >= 0;
-		 attroff--)
+	if (innerrel->rtekind == RTE_RELATION)
 	{
-		if (!bms_is_subset(innerrel->attr_needed[attroff], joinrelids))
+		/*
+		 * For a plain-relation innerrel, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's no point in going
+		 * further.
+		 */
+		if (innerrel->indexlist == NIL)
 			return false;
 	}
-
-	/*
-	 * Similarly check that the inner rel isn't needed by any PlaceHolderVars
-	 * that will be used above the join.  We only need to fail if such a PHV
-	 * actually references some inner-rel attributes; but the correct check
-	 * for that is relatively expensive, so we first check against ph_eval_at,
-	 * which must mention the inner rel if the PHV uses any inner-rel attrs as
-	 * non-lateral references.  Note that if the PHV's syntactic scope is just
-	 * the inner rel, we can't drop the rel even if the PHV is variable-free.
-	 */
-	foreach(l, root->placeholder_list)
+	else if (innerrel->rtekind == RTE_SUBQUERY)
 	{
-		PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(l);
+		subquery = root->simple_rte_array[innerrelid]->subquery;
 
-		if (bms_is_subset(phinfo->ph_needed, joinrelids))
-			continue;			/* PHV is not used above the join */
-		if (bms_overlap(phinfo->ph_lateral, innerrel->relids))
-			return false;		/* it references innerrel laterally */
-		if (!bms_overlap(phinfo->ph_eval_at, innerrel->relids))
-			continue;			/* it definitely doesn't reference innerrel */
-		if (bms_is_subset(phinfo->ph_eval_at, innerrel->relids))
-			return false;		/* there isn't any other place to eval PHV */
-		if (bms_overlap(pull_varnos((Node *) phinfo->ph_var->phexpr),
-						innerrel->relids))
-			return false;		/* it does reference innerrel */
+		/*
+		 * If the subquery has no qualities that support distinctness proofs
+		 * then there's no point in going further.
+		 */
+		if (!query_supports_distinctness(subquery))
+			return false;
 	}
-
-	return true;
-}
-
-/*
- * specialjoin_is_unique_join
- *		True if it can be proved that this special join will never produce
- *		more than 1 row per outer row, otherwise returns false if there is
- *		insufficient evidence to prove the join is unique.
- */
-static bool
-specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
-{
-	int			innerrelid;
-	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
-	Relids		joinrelids;
-	ListCell   *l;
-	List	   *clause_list = NIL;
-
-	/* check if we've already marked this join as unique on a previous call */
-	if (sjinfo->is_unique_join)
-		return true;
-
-	/* if there's more than 1 relation involved then punt */
-	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
-		return false;
-
-	innerrel = find_base_rel(root, innerrelid);
-
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of pulling out the join condition's columns,
-	 * make a quick check to eliminate cases in which we will surely be unable
-	 * to prove uniqueness of the innerrel.
-	 */
-	if (!rel_supports_distinctness(root, innerrel))
-		return false;
+	else
+		return false;			/* unsupported rtekind */
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -293,7 +212,9 @@ specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 * either the outer rel or a pseudoconstant.  If an operator is
 	 * mergejoinable then it behaves like equality for some btree opclass, so
 	 * it's what we want.  The mergejoinability test also eliminates clauses
-	 * containing volatile functions, which we couldn't depend on.
+	 * containing volatile functions, which we couldn't depend on.  The
+	 * uniquness might differ from the previous call on the same innerrel so
+	 * always check it regardless of the previous result.
 	 */
 	foreach(l, innerrel->joininfo)
 	{
@@ -309,8 +230,10 @@ specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then we can't
-			 * mark the join as unique.
+			 * If such a clause actually references the inner rel then join
+			 * removal has to be disallowed.  We have to check this despite
+			 * the previous attr_needed checks because of the possibility of
+			 * pushed-down clauses referencing the rel.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -333,10 +256,63 @@ specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	if (rel_is_distinct_for(root, innerrel, clause_list))
-		return true;
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	sjinfo->is_unique_join = rel_is_distinct_for(root, innerrel, clause_list);
 
-	return false;
+	if (!sjinfo->is_unique_join)
+		return false;
+
+	/*
+	 * We can't remove the join if any inner-rel attributes are used above the
+	 * join.
+	 *
+	 * Note that this test only detects use of inner-rel attributes in higher
+	 * join conditions and the target list.  There might be such attributes in
+	 * pushed-down conditions at this join, too.  We check that case below.
+	 *
+	 * As a micro-optimization, it seems better to start with max_attr and
+	 * count down rather than starting with min_attr and counting up, on the
+	 * theory that the system attributes are somewhat less likely to be wanted
+	 * and should be tested last.
+	 */
+	for (attroff = innerrel->max_attr - innerrel->min_attr;
+		 attroff >= 0 && removable;
+		 attroff--)
+	{
+		if (!bms_is_subset(innerrel->attr_needed[attroff], joinrelids))
+			return false;
+	}
+
+	/*
+	 * Similarly check that the inner rel isn't needed by any PlaceHolderVars
+	 * that will be used above the join.  We only need to fail if such a PHV
+	 * actually references some inner-rel attributes; but the correct check
+	 * for that is relatively expensive, so we first check against ph_eval_at,
+	 * which must mention the inner rel if the PHV uses any inner-rel attrs as
+	 * non-lateral references.  Note that if the PHV's syntactic scope is just
+	 * the inner rel, we can't drop the rel even if the PHV is variable-free.
+	 */
+	foreach(l, root->placeholder_list)
+	{
+		PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(l);
+
+		if (bms_is_subset(phinfo->ph_needed, joinrelids))
+			continue;			/* PHV is not used above the join */
+		if (bms_overlap(phinfo->ph_lateral, innerrel->relids))
+			return false;		/* it references innerrel laterally */
+		if (!bms_overlap(phinfo->ph_eval_at, innerrel->relids))
+			continue;			/* it definitely doesn't reference innerrel */
+		if (bms_is_subset(phinfo->ph_eval_at, innerrel->relids))
+			return false;		/* there isn't any other place to eval PHV */
+		if (bms_overlap(pull_varnos((Node *) phinfo->ph_var->phexpr),
+						innerrel->relids))
+			return false;		/* it does reference innerrel */
+	}
+
+	return true;
 }
 
 
@@ -617,43 +593,6 @@ rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
 	}
 	return false; /* can't prove rel to be distinct over clause_list */
 }
-/*
- * rel_supports_distinctness
- *		Returns true if rel has some properties which can prove the relation
- *		to be unique over some set of columns.
- *
- * This is effectively a pre-checking function for rel_is_distinct_for().
- * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
- */
-bool
-rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
-{
-	if (rel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's nothing to prove
-		 * uniqueness on the relation.
-		 */
-		if (rel->indexlist != NIL)
-			return true;
-	}
-	else if (rel->rtekind == RTE_SUBQUERY)
-	{
-		Query *subquery = root->simple_rte_array[rel->relid]->subquery;
-
-		/* Check if the subquery has any qualities that support distinctness */
-		if (query_supports_distinctness(subquery))
-			return true;
-	}
-
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
-	return false;
-}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 55310d8..848df97 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -174,9 +174,6 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
-	/* Analyze joins to find out which ones have a unique inner side */
-	mark_unique_joins(root, joinlist);
-
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 776a269..b7cecb6 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1407,6 +1407,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	List	   *mergejoinable_clauses; /* cache of mergejoinable clause */
 	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 7a85227..bd1a709 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -126,7 +126,6 @@ extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 								List *clause_list);
-extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
-- 
2.1.0.GIT

#25David Rowley
dgrowleyml@gmail.com
In reply to: Kyotaro HORIGUCHI (#24)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 25 March 2015 at 01:11, Kyotaro HORIGUCHI <
horiguchi.kyotaro@lab.ntt.co.jp> wrote:

Hi, thanks for the new patch.

I made an additional shrink from your last one. Do you have a
look on the attached?

Thanks, for looking again.

I'm not too sure I like the idea of relying on join removals to mark the
is_unique_join property.
By explicitly doing it in mark_unique_joins we have the nice side effect of
not having to re-check a relations unique properties after removing another
relation, providing the relation was already marked as unique on the first
pass.

At Sun, 22 Mar 2015 19:42:21 +1300, David Rowley <dgrowleyml@gmail.com>
wrote in <
CAApHDvrKwMmTwkXfn4uazYZA9jQL1c7UwBjBtuwFR69rqLVKfA@mail.gmail.com>

On 20 March 2015 at 21:11, David Rowley <dgrowleyml@gmail.com> wrote:

I can continue working on your patch if you like? Or are you planning

to

go further with it?

It's fine that you continue to work on this.

# Sorry for the hardly baked patch which had left many things alone:(

I've been working on this more over the weekend and I've re-factored

things

to allow LEFT JOINs to be properly marked as unique.
I've also made changes to re-add support for detecting the uniqueness of
sub-queries.

I don't see the point of calling mark_unique_joins for every
iteration on join_info_list in remove_useless_joins.

I've fixed this in the attached. I must have forgotten to put the test for
LEFT JOINs here as I was still thinking that I might make a change to the
code that converts unique semi joins to inner joins so that it just checks
is_unique_join instead of calling relation_has_unique_index_for().

The loop already iteraltes on whole the join_info_list so
mark_unique_join as an individual function is needless.

Finally, simply marking uniqueness of join in join_is_removable
seems to be enough, inhibiting early bailing out by the checks on
attribute usage and placeholder let it work as expected.

Reducing changes to this extent, I can easily see what is added
to planning computations. It consists of mainly two factors.

- Unique-join chekcs for every candidate inner joins in
add_paths_to_joinrel.

- Uniqueness check of mergejoinable clause in join-removability
check for every left join, some of which would be skipped
by other checks before.

Also, I've added modified the costing for hash and nested loop joins to
reduce the cost for unique inner joins to cost the join the same as it

does

for SEMI joins. This has tipped the scales on a few plans in the

regression

tests.

I've forgotten it, but quite important.

I've fixed quite a fundamental bug in my previous costing change. The fix
for this makes it so I have to pass the unique_inner bool down to the
costing functions. This also changes how the JoinPaths are marked as
unique_inner. This is now done when the JoinPath is created, instead of
updating it afterwards like it was done previously.

Also, please see attached unijoin_analysis.patch. This just adds some code

which spouts out notices when join nodes are initialised which states if
the join is unique or not. Running the regression tests with this patch

in

places gives:

Unique Inner: Yes == 753 hits
Unique Inner: No == 1430 hits

So it seems we can increase the speed of about 1 third of joins by about
10%.
A quick scan of the "No"s seems to show quite a few cases which do not

look

that real world like. e.g cartesian join.

I don't have an idea how many queries in the reality hit this but
I suppose it's not a few.

It would be great if someone could run some PostgreSQL application with
these 2 patches applied, and then grep the logs for the Unique Inner
results... Just to get a better idea of how many joins in a real world

case

will benefit from this patch.

Wow. I think the second patch should be DEBUGx, not NOTICE:)

I didn't give that much thought, but you're probably right. It was just a
quick test and demo to try to raise some more interest in this. :-)

Regards

David Rowley

Attachments:

unijoin_2015-03-28_d1923fb.patchapplication/octet-stream; name=unijoin_2015-03-28_d1923fb.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 315a528..ca4d912 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1151,9 +1151,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
 						appendStringInfo(es->str, " %s Join", jointype);
 					else if (!IsA(plan, NestLoop))
 						appendStringInfoString(es->str, " Join");
+					if (((Join *)plan)->unique_inner)
+						appendStringInfoString(es->str, "(inner unique)");
+
 				}
 				else
+				{
 					ExplainPropertyText("Join Type", jointype, es);
+					ExplainPropertyText("Inner unique",
+							((Join *)plan)->unique_inner ? "true" : "false", es);
+				}
 			}
 			break;
 		case T_SetOp:
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 1d78cdf..f2471aa 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple. For the case of SEMI joins, we want to skip to
+					 * the next outer row after having matched 1 inner row.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.unique_inner)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -451,6 +453,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate = makeNode(HashJoinState);
 	hjstate->js.ps.plan = (Plan *) node;
 	hjstate->js.ps.state = estate;
+	hjstate->js.unique_inner = node->join.unique_inner;
 
 	/*
 	 * Miscellaneous initialization
@@ -498,8 +501,10 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			hjstate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 15742c5..f6f0559 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,12 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple. For the case of SEMI joins, we want to skip to
+					 * the next outer row after having matched 1 inner row.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.unique_inner)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1486,6 +1488,8 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.plan = (Plan *) node;
 	mergestate->js.ps.state = estate;
 
+	mergestate->js.unique_inner = node->join.unique_inner;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -1553,8 +1557,10 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			mergestate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e66bcda..b5e6de1 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,12 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * We'll consider returning the first match if the inner is
+			 * unique, but after that we're done with this outer tuple.
+			 * For the case of SEMI joins, we want to skip to the next outer
+			 * row after having matched 1 inner row.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.unique_inner)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -310,6 +312,8 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.plan = (Plan *) node;
 	nlstate->js.ps.state = estate;
 
+	nlstate->js.unique_inner = node->join.unique_inner;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -354,8 +358,10 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			nlstate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 029761e..4008328 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1944,6 +1944,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(is_unique_join);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 190e50a..25885df 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -798,6 +798,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(is_unique_join);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 385b289..69e7353 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1948,6 +1948,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(is_unique_join);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 1a0d358..df34d71 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1712,7 +1712,7 @@ cost_group(Path *path, PlannerInfo *root,
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors)
@@ -1746,7 +1746,9 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI ||
+		unique_inner)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
@@ -1847,7 +1849,9 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of source data */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI ||
+		path->unique_inner)
 	{
 		double		outer_matched_rows = workspace->outer_matched_rows;
 		Selectivity inner_scan_frac = workspace->inner_scan_frac;
@@ -2658,7 +2662,9 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		path->jpath.unique_inner)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 1da953f..af9214d 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -18,6 +18,7 @@
 
 #include "executor/executor.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -28,18 +29,19 @@
 static void sort_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 List *restrictlist, List *mergeclause_list,
-					 JoinType jointype, SpecialJoinInfo *sjinfo,
-					 Relids param_source_rels, Relids extra_lateral_rels);
+					 JoinType jointype, bool unique_inner,
+					 SpecialJoinInfo *sjinfo, Relids param_source_rels,
+					 Relids extra_lateral_rels);
 static void match_unsorted_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 List *restrictlist, List *mergeclause_list,
-					 JoinType jointype, SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinType jointype, bool unique_inner,
+					 SpecialJoinInfo *sjinfo, SemiAntiJoinFactors *semifactors,
 					 Relids param_source_rels, Relids extra_lateral_rels);
 static void hash_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
-					 List *restrictlist,
-					 JoinType jointype, SpecialJoinInfo *sjinfo,
+					 List *restrictlist, JoinType jointype,
+					 bool unique_inner, SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
 					 Relids param_source_rels, Relids extra_lateral_rels);
 static List *select_mergejoin_clauses(PlannerInfo *root,
@@ -49,7 +51,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+										   RelOptInfo *innerrel);
 
 /*
  * add_paths_to_joinrel
@@ -89,6 +92,38 @@ add_paths_to_joinrel(PlannerInfo *root,
 	Relids		param_source_rels = NULL;
 	Relids		extra_lateral_rels = NULL;
 	ListCell   *lc;
+	bool		unique_inner;
+
+	/* left joins were already been analyzed for uniqueness in mark_unique_joins() */
+	if (jointype == JOIN_LEFT)
+		unique_inner = sjinfo->is_unique_join;
+	else if (jointype == JOIN_INNER &&
+		restrictlist != NIL &&
+		rel_supports_distinctness(root, innerrel))
+	{
+		/*
+		 * remember the number of items that were in the restrictlist as
+		 * the call to relation_has_unique_index_for may add more items
+		 * which we'll need to remove later.
+		 */
+		int org_len = list_length(restrictlist);
+
+		/*
+		 * rel_is_distinct_for requires restrict infos to have the
+		 * correct clause direction info
+		 */
+		foreach(lc, restrictlist)
+		{
+			clause_sides_match_join((RestrictInfo *)lfirst(lc),
+				outerrel, innerrel);
+		}
+		unique_inner = rel_is_distinct_for(root, innerrel, restrictlist);
+
+		/* Remove any list items added by rel_is_distinct_for */
+		list_truncate(restrictlist, org_len);
+	}
+	else
+		unique_inner = false;	/* we can't prove uniqueness */
 
 	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
@@ -214,7 +249,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	if (mergejoin_allowed)
 		sort_inner_and_outer(root, joinrel, outerrel, innerrel,
 							 restrictlist, mergeclause_list, jointype,
-							 sjinfo,
+							 unique_inner, sjinfo,
 							 param_source_rels, extra_lateral_rels);
 
 	/*
@@ -227,7 +262,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	if (mergejoin_allowed)
 		match_unsorted_outer(root, joinrel, outerrel, innerrel,
 							 restrictlist, mergeclause_list, jointype,
-							 sjinfo, &semifactors,
+							 unique_inner, sjinfo, &semifactors,
 							 param_source_rels, extra_lateral_rels);
 
 #ifdef NOT_USED
@@ -257,11 +292,12 @@ add_paths_to_joinrel(PlannerInfo *root,
 	 */
 	if (enable_hashjoin || jointype == JOIN_FULL)
 		hash_inner_and_outer(root, joinrel, outerrel, innerrel,
-							 restrictlist, jointype,
+							 restrictlist, jointype, unique_inner,
 							 sjinfo, &semifactors,
 							 param_source_rels, extra_lateral_rels);
 }
 
+
 /*
  * try_nestloop_path
  *	  Consider a nestloop join path; if it appears useful, push it into
@@ -271,6 +307,7 @@ static void
 try_nestloop_path(PlannerInfo *root,
 				  RelOptInfo *joinrel,
 				  JoinType jointype,
+				  bool unique_inner,
 				  SpecialJoinInfo *sjinfo,
 				  SemiAntiJoinFactors *semifactors,
 				  Relids param_source_rels,
@@ -332,7 +369,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * The latter two steps are expensive enough to make this two-phase
 	 * methodology worthwhile.
 	 */
-	initial_cost_nestloop(root, &workspace, jointype,
+	initial_cost_nestloop(root, &workspace, jointype, unique_inner,
 						  outer_path, inner_path,
 						  sjinfo, semifactors);
 
@@ -344,6 +381,7 @@ try_nestloop_path(PlannerInfo *root,
 				 create_nestloop_path(root,
 									  joinrel,
 									  jointype,
+									  unique_inner,
 									  &workspace,
 									  sjinfo,
 									  semifactors,
@@ -369,6 +407,7 @@ static void
 try_mergejoin_path(PlannerInfo *root,
 				   RelOptInfo *joinrel,
 				   JoinType jointype,
+				   bool unique_inner,
 				   SpecialJoinInfo *sjinfo,
 				   Relids param_source_rels,
 				   Relids extra_lateral_rels,
@@ -430,6 +469,7 @@ try_mergejoin_path(PlannerInfo *root,
 				 create_mergejoin_path(root,
 									   joinrel,
 									   jointype,
+									   unique_inner,
 									   &workspace,
 									   sjinfo,
 									   outer_path,
@@ -457,6 +497,7 @@ static void
 try_hashjoin_path(PlannerInfo *root,
 				  RelOptInfo *joinrel,
 				  JoinType jointype,
+				  bool unique_inner,
 				  SpecialJoinInfo *sjinfo,
 				  SemiAntiJoinFactors *semifactors,
 				  Relids param_source_rels,
@@ -505,6 +546,7 @@ try_hashjoin_path(PlannerInfo *root,
 				 create_hashjoin_path(root,
 									  joinrel,
 									  jointype,
+									  unique_inner,
 									  &workspace,
 									  sjinfo,
 									  semifactors,
@@ -565,6 +607,7 @@ clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
  * 'mergeclause_list' is a list of RestrictInfo nodes for available
  *		mergejoin clauses in this join
  * 'jointype' is the type of join to do
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'param_source_rels' are OK targets for parameterization of result paths
  * 'extra_lateral_rels' are additional parameterization for result paths
@@ -577,6 +620,7 @@ sort_inner_and_outer(PlannerInfo *root,
 					 List *restrictlist,
 					 List *mergeclause_list,
 					 JoinType jointype,
+					 bool unique_inner,
 					 SpecialJoinInfo *sjinfo,
 					 Relids param_source_rels,
 					 Relids extra_lateral_rels)
@@ -707,6 +751,7 @@ sort_inner_and_outer(PlannerInfo *root,
 		try_mergejoin_path(root,
 						   joinrel,
 						   jointype,
+						   unique_inner,
 						   sjinfo,
 						   param_source_rels,
 						   extra_lateral_rels,
@@ -752,6 +797,7 @@ sort_inner_and_outer(PlannerInfo *root,
  * 'mergeclause_list' is a list of RestrictInfo nodes for available
  *		mergejoin clauses in this join
  * 'jointype' is the type of join to do
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
  * 'param_source_rels' are OK targets for parameterization of result paths
@@ -765,6 +811,7 @@ match_unsorted_outer(PlannerInfo *root,
 					 List *restrictlist,
 					 List *mergeclause_list,
 					 JoinType jointype,
+					 bool unique_inner,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
 					 Relids param_source_rels,
@@ -895,6 +942,7 @@ match_unsorted_outer(PlannerInfo *root,
 			try_nestloop_path(root,
 							  joinrel,
 							  jointype,
+							  unique_inner,
 							  sjinfo,
 							  semifactors,
 							  param_source_rels,
@@ -921,6 +969,7 @@ match_unsorted_outer(PlannerInfo *root,
 				try_nestloop_path(root,
 								  joinrel,
 								  jointype,
+								  unique_inner,
 								  sjinfo,
 								  semifactors,
 								  param_source_rels,
@@ -936,6 +985,7 @@ match_unsorted_outer(PlannerInfo *root,
 				try_nestloop_path(root,
 								  joinrel,
 								  jointype,
+								  unique_inner,
 								  sjinfo,
 								  semifactors,
 								  param_source_rels,
@@ -993,6 +1043,7 @@ match_unsorted_outer(PlannerInfo *root,
 		try_mergejoin_path(root,
 						   joinrel,
 						   jointype,
+						   unique_inner,
 						   sjinfo,
 						   param_source_rels,
 						   extra_lateral_rels,
@@ -1092,6 +1143,7 @@ match_unsorted_outer(PlannerInfo *root,
 				try_mergejoin_path(root,
 								   joinrel,
 								   jointype,
+								   unique_inner,
 								   sjinfo,
 								   param_source_rels,
 								   extra_lateral_rels,
@@ -1138,6 +1190,7 @@ match_unsorted_outer(PlannerInfo *root,
 					try_mergejoin_path(root,
 									   joinrel,
 									   jointype,
+									   unique_inner,
 									   sjinfo,
 									   param_source_rels,
 									   extra_lateral_rels,
@@ -1172,6 +1225,7 @@ match_unsorted_outer(PlannerInfo *root,
  * 'restrictlist' contains all of the RestrictInfo nodes for restriction
  *		clauses that apply to this join
  * 'jointype' is the type of join to do
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
  * 'param_source_rels' are OK targets for parameterization of result paths
@@ -1184,6 +1238,7 @@ hash_inner_and_outer(PlannerInfo *root,
 					 RelOptInfo *innerrel,
 					 List *restrictlist,
 					 JoinType jointype,
+					 bool unique_inner,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
 					 Relids param_source_rels,
@@ -1258,6 +1313,7 @@ hash_inner_and_outer(PlannerInfo *root,
 			try_hashjoin_path(root,
 							  joinrel,
 							  jointype,
+							  unique_inner,
 							  sjinfo,
 							  semifactors,
 							  param_source_rels,
@@ -1278,6 +1334,7 @@ hash_inner_and_outer(PlannerInfo *root,
 			try_hashjoin_path(root,
 							  joinrel,
 							  jointype,
+							  unique_inner,
 							  sjinfo,
 							  semifactors,
 							  param_source_rels,
@@ -1291,6 +1348,7 @@ hash_inner_and_outer(PlannerInfo *root,
 				try_hashjoin_path(root,
 								  joinrel,
 								  jointype,
+								  unique_inner,
 								  sjinfo,
 								  semifactors,
 								  param_source_rels,
@@ -1316,6 +1374,7 @@ hash_inner_and_outer(PlannerInfo *root,
 				try_hashjoin_path(root,
 								  joinrel,
 								  jointype,
+								  unique_inner,
 								  sjinfo,
 								  semifactors,
 								  param_source_rels,
@@ -1354,6 +1413,7 @@ hash_inner_and_outer(PlannerInfo *root,
 					try_hashjoin_path(root,
 									  joinrel,
 									  jointype,
+									  unique_inner,
 									  sjinfo,
 									  semifactors,
 									  param_source_rels,
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index fe9fd57..a79c194 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -624,6 +624,7 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 		/* we don't bother trying to make the remaining fields valid */
 		sjinfo->lhs_strict = false;
 		sjinfo->delay_upper_joins = false;
+		sjinfo->is_unique_join = false;
 		sjinfo->semi_can_btree = false;
 		sjinfo->semi_can_hash = false;
 		sjinfo->semi_operators = NIL;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 11d3933..13a561a 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -33,11 +33,37 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+/*
+ * mark_unique_joins
+		Analyze joins in order to determine if their inner side is unique based
+		on the join condition.
+ */
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs that have not already
+		 * been marked as unique by a previous call.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT &&
+			!sjinfo->is_unique_join &&
+			specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -91,6 +117,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to mark some joins as unique which we could
+		 * not do before
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -151,17 +183,17 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must not duplicate its outer side and must be a non-delaying left
+	 * join to a single baserel, else we aren't going to be able to do anything
+	 * with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->is_unique_join ||
+		sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -170,38 +202,7 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -212,7 +213,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -253,6 +255,45 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most 1 inner row for any single outer row. False is returned if there's
+ *		insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Query	   *subquery = NULL;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -274,10 +315,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we can't
+			 * mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -300,71 +339,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -564,6 +541,125 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list, callers should either
+ * make a copy of the list or trim it back to it's original length after
+ * calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *)lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *)rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *)get_rightop(rinfo->clause);
+			else
+				var = (Var *)get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false; /* can't prove rel to be distinct over clause_list */
+}
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's nothing to prove
+		 * uniqueness on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index cb69c03..02899de 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -131,13 +131,12 @@ static BitmapAnd *make_bitmap_and(List *bitmapplans);
 static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
-			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  Plan *lefttree, Plan *righttree, JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -152,7 +151,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2192,7 +2191,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->path);
 
@@ -2486,7 +2485,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
@@ -2612,7 +2611,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3717,7 +3716,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3727,8 +3726,9 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 	node->nestParams = nestParams;
 
 	return node;
@@ -3741,7 +3741,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3752,8 +3752,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
@@ -3801,7 +3802,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3816,8 +3817,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index a7655e4..8094880 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1087,6 +1087,7 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	sjinfo->is_unique_join = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 848df97..55310d8 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -174,6 +174,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Analyze joins to find out which ones have a unique inner side */
+	mark_unique_joins(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index faca30b..3cb8644 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1514,6 +1514,7 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  *
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_nestloop
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1529,6 +1530,7 @@ NestPath *
 create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1581,6 +1583,7 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->jointype = jointype;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
+	pathnode->unique_inner = unique_inner;
 	pathnode->joinrestrictinfo = restrict_clauses;
 
 	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
@@ -1595,6 +1598,7 @@ create_nestloop_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+  * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_mergejoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'outer_path' is the outer path
@@ -1611,6 +1615,7 @@ MergePath *
 create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -1638,6 +1643,7 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
@@ -1655,6 +1661,7 @@ create_mergejoin_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_hashjoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1669,6 +1676,7 @@ HashPath *
 create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1706,6 +1714,7 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index ac75f86..e79cf19 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1565,6 +1565,7 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 21cbfa8..f093541 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -543,6 +543,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 401a686..a628e4f 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1031,6 +1031,7 @@ typedef struct JoinPath
 
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
@@ -1407,6 +1408,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 9c2000b..307977e 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -113,7 +113,7 @@ extern void cost_group(Path *path, PlannerInfo *root,
 		   double input_tuples);
 extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 9923f0e..adebd70 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -89,6 +89,7 @@ extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_pat
 extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -101,6 +102,7 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -115,6 +117,7 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index fa72918..7a85227 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -122,7 +122,11 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+								List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index dfae84e..ad1d673 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -186,7 +186,7 @@ explain (costs off)
   select * from ec1, ec2 where ff = x1 and x1 = '42'::int8alias2;
                QUERY PLAN                
 -----------------------------------------
- Nested Loop
+ Nested Loop(inner unique)
    ->  Seq Scan on ec2
          Filter: (x1 = '42'::int8alias2)
    ->  Index Scan using ec1_pkey on ec1
@@ -310,7 +310,7 @@ explain (costs off)
          ->  Index Scan using ec1_expr3 on ec1 ec1_5
          ->  Index Scan using ec1_expr4 on ec1 ec1_6
    ->  Materialize
-         ->  Merge Join
+         ->  Merge Join(inner unique)
                Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
                ->  Merge Append
                      Sort Key: (((ec1_1.ff + 2) + 1))
@@ -365,7 +365,7 @@ explain (costs off)
   where ss1.x = ec1.f1 and ec1.ff = 42::int8;
                      QUERY PLAN                      
 -----------------------------------------------------
- Merge Join
+ Merge Join(inner unique)
    Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
    ->  Merge Append
          Sort Key: (((ec1_1.ff + 2) + 1))
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 57fc910..6140eba 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2614,8 +2614,8 @@ from nt3 as nt3
 where nt3.id = 1 and ss2.b3;
                   QUERY PLAN                   
 -----------------------------------------------
- Nested Loop
-   ->  Nested Loop
+ Nested Loop(inner unique)
+   ->  Nested Loop(inner unique)
          ->  Index Scan using nt3_pkey on nt3
                Index Cond: (id = 1)
          ->  Index Scan using nt2_pkey on nt2
@@ -3338,7 +3338,7 @@ explain (costs off)
     on (p.k = ss.k);
            QUERY PLAN            
 ---------------------------------
- Hash Left Join
+ Hash Left Join(inner unique)
    Hash Cond: (p.k = c.k)
    ->  Seq Scan on parent p
    ->  Hash
@@ -4416,3 +4416,247 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join(inner unique)
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(9 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join(inner unique)
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join(inner unique)
+   Output: j1.id, j2.id
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(8 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join(inner unique)
+   Output: j1.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop(inner unique)
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(12 rows)
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop(inner unique)
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(12 rows)
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+         QUERY PLAN          
+-----------------------------
+ Hash Join
+   Output: j1.id, (1)
+   Hash Cond: (j1.id = (1))
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: (1)
+         ->  Result
+               Output: 1
+(9 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(7 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop(inner unique)
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 44e8dab..9517f9b 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -248,7 +248,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle);
 EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
                      QUERY PLAN                     
 ----------------------------------------------------
- Nested Loop
+ Nested Loop(inner unique)
    ->  Subquery Scan on document
          Filter: f_leak(document.dtitle)
          ->  Seq Scan on document document_1
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 82d510d..01b8b45 100644
--- a/src/test/regress/expected/select_views.out
+++ b/src/test/regress/expected/select_views.out
@@ -1365,7 +1365,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Join(inner unique)
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1386,7 +1386,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Join(inner unique)
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1420,7 +1420,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1451,7 +1451,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/expected/select_views_1.out b/src/test/regress/expected/select_views_1.out
index ce22bfa..a37bde4 100644
--- a/src/test/regress/expected/select_views_1.out
+++ b/src/test/regress/expected/select_views_1.out
@@ -1365,7 +1365,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Join(inner unique)
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1386,7 +1386,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Join(inner unique)
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1420,7 +1420,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1451,7 +1451,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 06a27ea..9d27d1e 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1307,3 +1307,98 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#26Alexander Korotkov
a.korotkov@postgrespro.ru
In reply to: David Rowley (#25)
Re: Performance improvement for joins where outer side is unique

Hi!

On Sat, Mar 28, 2015 at 10:35 AM, David Rowley <dgrowleyml@gmail.com> wrote:

On 25 March 2015 at 01:11, Kyotaro HORIGUCHI <
horiguchi.kyotaro@lab.ntt.co.jp> wrote:

Hi, thanks for the new patch.

I made an additional shrink from your last one. Do you have a
look on the attached?

Thanks, for looking again.

I'm not too sure I like the idea of relying on join removals to mark the
is_unique_join property.
By explicitly doing it in mark_unique_joins we have the nice side effect
of not having to re-check a relations unique properties after removing
another relation, providing the relation was already marked as unique on
the first pass.

At Sun, 22 Mar 2015 19:42:21 +1300, David Rowley <dgrowleyml@gmail.com>
wrote in <
CAApHDvrKwMmTwkXfn4uazYZA9jQL1c7UwBjBtuwFR69rqLVKfA@mail.gmail.com>

On 20 March 2015 at 21:11, David Rowley <dgrowleyml@gmail.com> wrote:

I can continue working on your patch if you like? Or are you planning

to

go further with it?

It's fine that you continue to work on this.

# Sorry for the hardly baked patch which had left many things alone:(

I've been working on this more over the weekend and I've re-factored

things

to allow LEFT JOINs to be properly marked as unique.
I've also made changes to re-add support for detecting the uniqueness of
sub-queries.

I don't see the point of calling mark_unique_joins for every
iteration on join_info_list in remove_useless_joins.

I've fixed this in the attached. I must have forgotten to put the test for
LEFT JOINs here as I was still thinking that I might make a change to the
code that converts unique semi joins to inner joins so that it just checks
is_unique_join instead of calling relation_has_unique_index_for().

Patch doesn't apply to current master. Could you, please, rebase it?

patching file src/backend/commands/explain.c
Hunk #1 succeeded at 1193 (offset 42 lines).
patching file src/backend/executor/nodeHashjoin.c
patching file src/backend/executor/nodeMergejoin.c
patching file src/backend/executor/nodeNestloop.c
patching file src/backend/nodes/copyfuncs.c
Hunk #1 succeeded at 2026 (offset 82 lines).
patching file src/backend/nodes/equalfuncs.c
Hunk #1 succeeded at 837 (offset 39 lines).
patching file src/backend/nodes/outfuncs.c
Hunk #1 succeeded at 2010 (offset 62 lines).
patching file src/backend/optimizer/path/costsize.c
Hunk #1 succeeded at 1780 (offset 68 lines).
Hunk #2 succeeded at 1814 with fuzz 2 (offset 68 lines).
Hunk #3 succeeded at 1887 with fuzz 2 (offset 38 lines).
Hunk #4 succeeded at 2759 (offset 97 lines).
patching file src/backend/optimizer/path/joinpath.c
Hunk #1 succeeded at 19 with fuzz 2 (offset 1 line).
Hunk #2 FAILED at 30.
Hunk #3 succeeded at 46 (offset -5 lines).
Hunk #4 succeeded at 84 with fuzz 2 (offset -8 lines).
Hunk #5 FAILED at 241.
Hunk #6 FAILED at 254.
Hunk #7 FAILED at 284.
Hunk #8 FAILED at 299.
Hunk #9 succeeded at 373 with fuzz 2 (offset 4 lines).
Hunk #10 succeeded at 385 with fuzz 2 (offset 4 lines).
Hunk #11 FAILED at 411.
Hunk #12 succeeded at 470 with fuzz 2 (offset 1 line).
Hunk #13 FAILED at 498.
Hunk #14 succeeded at 543 with fuzz 2 (offset -3 lines).
Hunk #15 FAILED at 604.
Hunk #16 FAILED at 617.
Hunk #17 FAILED at 748.
Hunk #18 FAILED at 794.
Hunk #19 FAILED at 808.
Hunk #20 FAILED at 939.
Hunk #21 FAILED at 966.
Hunk #22 FAILED at 982.
Hunk #23 FAILED at 1040.
Hunk #24 FAILED at 1140.
Hunk #25 FAILED at 1187.
Hunk #26 FAILED at 1222.
Hunk #27 FAILED at 1235.
Hunk #28 FAILED at 1310.
Hunk #29 FAILED at 1331.
Hunk #30 FAILED at 1345.
Hunk #31 FAILED at 1371.
Hunk #32 FAILED at 1410.
25 out of 32 hunks FAILED -- saving rejects to file
src/backend/optimizer/path/joinpath.c.rej
patching file src/backend/optimizer/path/joinrels.c
patching file src/backend/optimizer/plan/analyzejoins.c
patching file src/backend/optimizer/plan/createplan.c
Hunk #1 succeeded at 135 (offset 4 lines).
Hunk #2 succeeded at 155 (offset 4 lines).
Hunk #3 succeeded at 2304 (offset 113 lines).
Hunk #4 succeeded at 2598 (offset 113 lines).
Hunk #5 succeeded at 2724 (offset 113 lines).
Hunk #6 succeeded at 3855 (offset 139 lines).
Hunk #7 succeeded at 3865 (offset 139 lines).
Hunk #8 succeeded at 3880 (offset 139 lines).
Hunk #9 succeeded at 3891 (offset 139 lines).
Hunk #10 succeeded at 3941 (offset 139 lines).
Hunk #11 succeeded at 3956 (offset 139 lines).
patching file src/backend/optimizer/plan/initsplan.c
patching file src/backend/optimizer/plan/planmain.c
patching file src/backend/optimizer/util/pathnode.c
Hunk #1 succeeded at 1541 (offset 27 lines).
Hunk #2 succeeded at 1557 (offset 27 lines).
Hunk #3 succeeded at 1610 (offset 27 lines).
Hunk #4 succeeded at 1625 (offset 27 lines).
Hunk #5 succeeded at 1642 (offset 27 lines).
Hunk #6 succeeded at 1670 (offset 27 lines).
Hunk #7 succeeded at 1688 (offset 27 lines).
Hunk #8 succeeded at 1703 (offset 27 lines).
Hunk #9 succeeded at 1741 (offset 27 lines).
patching file src/include/nodes/execnodes.h
Hunk #1 succeeded at 1636 (offset 71 lines).
patching file src/include/nodes/plannodes.h
Hunk #1 succeeded at 586 (offset 43 lines).
patching file src/include/nodes/relation.h
Hunk #1 succeeded at 1045 (offset 14 lines).
Hunk #2 succeeded at 1422 (offset 14 lines).
patching file src/include/optimizer/cost.h
Hunk #1 succeeded at 114 (offset 1 line).
patching file src/include/optimizer/pathnode.h
Hunk #1 succeeded at 91 (offset 2 lines).
Hunk #2 succeeded at 104 (offset 2 lines).
Hunk #3 succeeded at 119 (offset 2 lines).
patching file src/include/optimizer/planmain.h
Hunk #1 succeeded at 124 (offset 2 lines).
patching file src/test/regress/expected/equivclass.out
patching file src/test/regress/expected/join.out
Hunk #1 succeeded at 2656 (offset 42 lines).
Hunk #2 succeeded at 3428 (offset 90 lines).
Hunk #3 succeeded at 4506 (offset 90 lines).
patching file src/test/regress/expected/rowsecurity.out
Hunk #1 succeeded at 274 (offset 26 lines).
patching file src/test/regress/expected/select_views.out
Hunk #1 succeeded at 1411 (offset 46 lines).
Hunk #2 succeeded at 1432 (offset 46 lines).
Hunk #3 succeeded at 1466 (offset 46 lines).
Hunk #4 succeeded at 1497 (offset 46 lines).
patching file src/test/regress/expected/select_views_1.out
Hunk #1 succeeded at 1411 (offset 46 lines).
Hunk #2 succeeded at 1432 (offset 46 lines).
Hunk #3 succeeded at 1466 (offset 46 lines).
Hunk #4 succeeded at 1497 (offset 46 lines).
patching file src/test/regress/sql/join.sql
Hunk #1 succeeded at 1344 (offset 37 lines).

------
Alexander Korotkov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#27David Rowley
david.rowley@2ndquadrant.com
In reply to: Alexander Korotkov (#26)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 8 July 2015 at 02:00, Alexander Korotkov <a.korotkov@postgrespro.ru>
wrote:

Patch doesn't apply to current master. Could you, please, rebase it?

Attached. Thanks.

Regards

David Rowley

--
David Rowley http://www.2ndQuadrant.com/
<http://www.2ndquadrant.com/&gt;
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_9e6d4e4_2015-07-08.patchapplication/octet-stream; name=unique_joins_9e6d4e4_2015-07-08.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 0d1ecc2..72518cb4 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1193,9 +1193,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
 						appendStringInfo(es->str, " %s Join", jointype);
 					else if (!IsA(plan, NestLoop))
 						appendStringInfoString(es->str, " Join");
+					if (((Join *)plan)->unique_inner)
+						appendStringInfoString(es->str, "(inner unique)");
+
 				}
 				else
+				{
 					ExplainPropertyText("Join Type", jointype, es);
+					ExplainPropertyText("Inner unique",
+							((Join *)plan)->unique_inner ? "true" : "false", es);
+				}
 			}
 			break;
 		case T_SetOp:
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 1d78cdf..f2471aa 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple. For the case of SEMI joins, we want to skip to
+					 * the next outer row after having matched 1 inner row.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.unique_inner)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -451,6 +453,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate = makeNode(HashJoinState);
 	hjstate->js.ps.plan = (Plan *) node;
 	hjstate->js.ps.state = estate;
+	hjstate->js.unique_inner = node->join.unique_inner;
 
 	/*
 	 * Miscellaneous initialization
@@ -498,8 +501,10 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			hjstate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 34b6cf6..100f085 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,12 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple. For the case of SEMI joins, we want to skip to
+					 * the next outer row after having matched 1 inner row.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.unique_inner)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1486,6 +1488,8 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.plan = (Plan *) node;
 	mergestate->js.ps.state = estate;
 
+	mergestate->js.unique_inner = node->join.unique_inner;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -1553,8 +1557,10 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			mergestate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e66bcda..b5e6de1 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,12 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * We'll consider returning the first match if the inner is
+			 * unique, but after that we're done with this outer tuple.
+			 * For the case of SEMI joins, we want to skip to the next outer
+			 * row after having matched 1 inner row.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.unique_inner)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -310,6 +312,8 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.plan = (Plan *) node;
 	nlstate->js.ps.state = estate;
 
+	nlstate->js.unique_inner = node->join.unique_inner;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -354,8 +358,10 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			nlstate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 4c363d3..0cb67a7 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2026,6 +2026,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(is_unique_join);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index f19251e..a89c075 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -837,6 +837,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(is_unique_join);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 87304ba..d023eea 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2010,6 +2010,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(is_unique_join);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 0d302f6..f041396 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1780,7 +1780,7 @@ cost_group(Path *path, PlannerInfo *root,
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors)
@@ -1814,7 +1814,9 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI ||
+		unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -1885,7 +1887,9 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI ||
+		path->unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -2755,7 +2759,9 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		path->jpath.unique_inner)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index ba78252..c585341 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -44,7 +45,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+										   RelOptInfo *innerrel);
 
 /*
  * add_paths_to_joinrel
@@ -81,12 +83,45 @@ add_paths_to_joinrel(PlannerInfo *root,
 	JoinPathExtraData extra;
 	bool		mergejoin_allowed = true;
 	ListCell   *lc;
+	bool		unique_inner;
+
+	/* left joins were already been analyzed for uniqueness in mark_unique_joins() */
+	if (jointype == JOIN_LEFT)
+		unique_inner = sjinfo->is_unique_join;
+	else if (jointype == JOIN_INNER &&
+		restrictlist != NIL &&
+		rel_supports_distinctness(root, innerrel))
+	{
+		/*
+		 * remember the number of items that were in the restrictlist as
+		 * the call to relation_has_unique_index_for may add more items
+		 * which we'll need to remove later.
+		 */
+		int org_len = list_length(restrictlist);
+
+		/*
+		 * rel_is_distinct_for requires restrict infos to have the
+		 * correct clause direction info
+		 */
+		foreach(lc, restrictlist)
+		{
+			clause_sides_match_join((RestrictInfo *)lfirst(lc),
+				outerrel, innerrel);
+		}
+		unique_inner = rel_is_distinct_for(root, innerrel, restrictlist);
+
+		/* Remove any list items added by rel_is_distinct_for */
+		list_truncate(restrictlist, org_len);
+	}
+	else
+		unique_inner = false;	/* we can't prove uniqueness */
 
 	extra.restrictlist = restrictlist;
 	extra.mergeclause_list = NIL;
 	extra.sjinfo = sjinfo;
 	extra.param_source_rels = NULL;
 	extra.extra_lateral_rels = NULL;
+	extra.unique_inner = unique_inner;
 
 	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
@@ -336,7 +371,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * The latter two steps are expensive enough to make this two-phase
 	 * methodology worthwhile.
 	 */
-	initial_cost_nestloop(root, &workspace, jointype,
+	initial_cost_nestloop(root, &workspace, jointype, extra->unique_inner,
 						  outer_path, inner_path,
 						  extra->sjinfo, &extra->semifactors);
 
@@ -348,6 +383,7 @@ try_nestloop_path(PlannerInfo *root,
 				 create_nestloop_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
@@ -431,6 +467,7 @@ try_mergejoin_path(PlannerInfo *root,
 				 create_mergejoin_path(root,
 									   joinrel,
 									   jointype,
+									   extra->unique_inner,
 									   &workspace,
 									   extra->sjinfo,
 									   outer_path,
@@ -502,6 +539,7 @@ try_hashjoin_path(PlannerInfo *root,
 				 create_hashjoin_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index fe9fd57..a79c194 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -624,6 +624,7 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 		/* we don't bother trying to make the remaining fields valid */
 		sjinfo->lhs_strict = false;
 		sjinfo->delay_upper_joins = false;
+		sjinfo->is_unique_join = false;
 		sjinfo->semi_can_btree = false;
 		sjinfo->semi_can_hash = false;
 		sjinfo->semi_operators = NIL;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 470db87..48ffbeb 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -33,11 +33,37 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+/*
+ * mark_unique_joins
+		Analyze joins in order to determine if their inner side is unique based
+		on the join condition.
+ */
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs that have not already
+		 * been marked as unique by a previous call.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT &&
+			!sjinfo->is_unique_join &&
+			specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -91,6 +117,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to mark some joins as unique which we could
+		 * not do before
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -151,17 +183,17 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must not duplicate its outer side and must be a non-delaying left
+	 * join to a single baserel, else we aren't going to be able to do anything
+	 * with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->is_unique_join ||
+		sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -170,38 +202,7 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -212,7 +213,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -253,6 +255,45 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most 1 inner row for any single outer row. False is returned if there's
+ *		insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Query	   *subquery = NULL;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -274,10 +315,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we can't
+			 * mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -300,71 +339,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -564,6 +541,125 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list, callers should either
+ * make a copy of the list or trim it back to it's original length after
+ * calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *)lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *)rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *)get_rightop(rinfo->clause);
+			else
+				var = (Var *)get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false; /* can't prove rel to be distinct over clause_list */
+}
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's nothing to prove
+		 * uniqueness on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index dc2dcbf..5801ea8 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -135,13 +135,12 @@ static BitmapAnd *make_bitmap_and(List *bitmapplans);
 static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
-			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  Plan *lefttree, Plan *righttree, JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -156,7 +155,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2305,7 +2304,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->path);
 
@@ -2599,7 +2598,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
@@ -2725,7 +2724,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3856,7 +3855,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3866,8 +3865,9 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 	node->nestParams = nestParams;
 
 	return node;
@@ -3880,7 +3880,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3891,8 +3891,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
@@ -3940,7 +3941,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3955,8 +3956,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 00b2625..d9553fd 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1087,6 +1087,7 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	sjinfo->is_unique_join = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 848df97..55310d8 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -174,6 +174,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Analyze joins to find out which ones have a unique inner side */
+	mark_unique_joins(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index f7f33bb..eb0a489 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1541,6 +1541,7 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  *
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_nestloop
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1556,6 +1557,7 @@ NestPath *
 create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1608,6 +1610,7 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->jointype = jointype;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
+	pathnode->unique_inner = unique_inner;
 	pathnode->joinrestrictinfo = restrict_clauses;
 
 	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
@@ -1622,6 +1625,7 @@ create_nestloop_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+  * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_mergejoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'outer_path' is the outer path
@@ -1638,6 +1642,7 @@ MergePath *
 create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -1665,6 +1670,7 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
@@ -1682,6 +1688,7 @@ create_mergejoin_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_hashjoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1696,6 +1703,7 @@ HashPath *
 create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1733,6 +1741,7 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 541ee18..80d6009 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1636,6 +1636,7 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 5f538f3..6058477 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -586,6 +586,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index cb916ea..1581e10 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1045,6 +1045,7 @@ typedef struct JoinPath
 
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
@@ -1421,6 +1422,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -1699,6 +1701,7 @@ typedef struct JoinPathExtraData
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
 	Relids		extra_lateral_rels;
+	bool		unique_inner;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 24003ae..dd8cf01 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -114,7 +114,7 @@ extern void cost_group(Path *path, PlannerInfo *root,
 		   double input_tuples);
 extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 161644c..e9b547d 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -91,6 +91,7 @@ extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_pat
 extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -103,6 +104,7 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -117,6 +119,7 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 52b077a..d7d373b 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -124,7 +124,11 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+								List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 0391b8e..511630c 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -186,7 +186,7 @@ explain (costs off)
   select * from ec1, ec2 where ff = x1 and x1 = '42'::int8alias2;
                QUERY PLAN                
 -----------------------------------------
- Nested Loop
+ Nested Loop(inner unique)
    ->  Seq Scan on ec2
          Filter: (x1 = '42'::int8alias2)
    ->  Index Scan using ec1_pkey on ec1
@@ -310,7 +310,7 @@ explain (costs off)
          ->  Index Scan using ec1_expr3 on ec1 ec1_5
          ->  Index Scan using ec1_expr4 on ec1 ec1_6
    ->  Materialize
-         ->  Merge Join
+         ->  Merge Join(inner unique)
                Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
                ->  Merge Append
                      Sort Key: (((ec1_1.ff + 2) + 1))
@@ -365,7 +365,7 @@ explain (costs off)
   where ss1.x = ec1.f1 and ec1.ff = 42::int8;
                      QUERY PLAN                      
 -----------------------------------------------------
- Merge Join
+ Merge Join(inner unique)
    Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
    ->  Merge Append
          Sort Key: (((ec1_1.ff + 2) + 1))
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 139f7e0..cc0cb25 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2656,8 +2656,8 @@ from nt3 as nt3
 where nt3.id = 1 and ss2.b3;
                   QUERY PLAN                   
 -----------------------------------------------
- Nested Loop
-   ->  Nested Loop
+ Nested Loop(inner unique)
+   ->  Nested Loop(inner unique)
          ->  Index Scan using nt3_pkey on nt3
                Index Cond: (id = 1)
          ->  Index Scan using nt2_pkey on nt2
@@ -3428,7 +3428,7 @@ explain (costs off)
     on (p.k = ss.k);
            QUERY PLAN            
 ---------------------------------
- Hash Left Join
+ Hash Left Join(inner unique)
    Hash Cond: (p.k = c.k)
    ->  Seq Scan on parent p
    ->  Hash
@@ -4506,3 +4506,247 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join(inner unique)
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(9 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join(inner unique)
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join(inner unique)
+   Output: j1.id, j2.id
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(8 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join(inner unique)
+   Output: j1.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop(inner unique)
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(12 rows)
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop(inner unique)
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(12 rows)
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+         QUERY PLAN          
+-----------------------------
+ Hash Join
+   Output: j1.id, (1)
+   Hash Cond: (j1.id = (1))
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: (1)
+         ->  Result
+               Output: 1
+(9 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(7 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop(inner unique)
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 4073c1b..781e38b 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -274,7 +274,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle);
 EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
                      QUERY PLAN                     
 ----------------------------------------------------
- Nested Loop
+ Nested Loop(inner unique)
    ->  Subquery Scan on document
          Filter: f_leak(document.dtitle)
          ->  Seq Scan on document document_1
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 7f57526..f8d0d60 100644
--- a/src/test/regress/expected/select_views.out
+++ b/src/test/regress/expected/select_views.out
@@ -1411,7 +1411,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Join(inner unique)
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1432,7 +1432,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Join(inner unique)
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1466,7 +1466,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1497,7 +1497,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/expected/select_views_1.out b/src/test/regress/expected/select_views_1.out
index 5275ef0..1ee18a1 100644
--- a/src/test/regress/expected/select_views_1.out
+++ b/src/test/regress/expected/select_views_1.out
@@ -1411,7 +1411,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Join(inner unique)
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1432,7 +1432,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Join(inner unique)
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1466,7 +1466,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1497,7 +1497,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Join(inner unique)
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 5b65ea8..3af0bdd 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1344,3 +1344,98 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#28Uriy Zhuravlev
u.zhuravlev@postgrespro.ru
In reply to: David Rowley (#27)
Re: Performance improvement for joins where outer side is unique

On Wednesday 08 July 2015 12:29:38 David Rowley wrote:

On 8 July 2015 at 02:00, Alexander Korotkov <a.korotkov@postgrespro.ru>

wrote:

Patch doesn't apply to current master. Could you, please, rebase it?

Attached. Thanks.

Regards

David Rowley

--
David Rowley http://www.2ndQuadrant.com/
<http://www.2ndquadrant.com/&gt;
PostgreSQL Development, 24x7 Support, Training & Services

Hello.
What is the current status of the patch?

--
YUriy Zhuravlev
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

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

#29Erik Rijkers
er@xs4all.nl
In reply to: Uriy Zhuravlev (#28)
Re: Performance improvement for joins where outer side is unique

On 2015-08-06 15:36, Uriy Zhuravlev wrote:

On Wednesday 08 July 2015 12:29:38 David Rowley wrote:

On 8 July 2015 at 02:00, Alexander Korotkov
<a.korotkov@postgrespro.ru>

wrote:

Patch doesn't apply to current master. Could you, please, rebase it?

Attached. Thanks.

Regards

David Rowley

--
David Rowley http://www.2ndQuadrant.com/
<http://www.2ndquadrant.com/&gt;
PostgreSQL Development, 24x7 Support, Training & Services

Hello.
What is the current status of the patch?

[unique_joins_9e6d4e4_2015-07-08.patch]

FWIW, I just happened to be looking at the latest patch:

applies OK (against current HEAD)
compiles OK
make check fail:
join ... FAILED
installs OK

I was just going to repeat David's 2 queries (hardware: x86_64, Linux
2.6.18-402.el5)

-- with these tables:
create table t1 (id int primary key);
create table t2 (id int primary key);
insert into t1 select x.x from generate_series(1,1000000) x(x);
insert into t2 select x.x from generate_series(1,1000000) x(x);

create table s1 (id varchar(32) primary key);
insert into s1 select x.x::varchar from generate_series(1,1000000) x(x);
create table s2 (id varchar(32) primary key);
insert into s2 select x.x::varchar from generate_series(1,1000000) x(x);

vacuum analyze;

I do not see much difference between patched and unpatched
after running both David's statements (unijoin.sql and unijoin2.sql):

-- pgbench -f unijoin.sql -n -T 300 -P30 testdb
select count(t2.id) from t1 left outer join t2 on t1.id=t2.id;
tps = 2.323222 - unpatched
tps = 2.356906 - patched

-- -- pgbench -f unijoin2.sql -n -T 300 -P30 testdb
tps = 1.257656 - unpatched
tps = 1.225758 - patched

So as far as I can tell it does not look very promising.

Erik Rijkers

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

#30Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Erik Rijkers (#29)
2 attachment(s)
Re: Performance improvement for joins where outer side is unique

Hi,

I did some initial performance evaluation of this patch today, and I see
a clear improvement on larger joins. The scenario I've chosen for the
experiments is a simple fact-dimension join, i.e. a small table
referenced by a large table. So effectively something like this:

CREATE TABLE dim (id INT PRIMARY KEY, ...);
CREATE TABLE fact (dim_d INT REFERENCES dim(id), ...);

except that I haven't used the foreign key constraint. In all the
experiments I've used a fact table 10x the size of the dimension, but I
believe what really matters most is the size of the dimension (and how
the hash table fits into the L2/L3 cache).

The query tested is very simple:

select count(1) from (
select * from f join d on (f.fact_id = d.dim_id)
) foo;

The outer aggregation is intentional - the join produces many rows and
formatting them as string would completely eliminate any gains from the
patch (even with "\o /dev/null" or such).

The following numbers come current master, running on E5-2630 v3
(2.40GHz), 64GB of RAM and this configuration:

checkpoint_timeout = 15min
effective_cache_size = 48GB
maintenance_work_mem = 1GB
max_wal_size = 4GB
min_wal_size = 1GB
random_page_cost = 1.5
shared_buffers = 4GB
work_mem = 1GB

all the other values are set to default.

I did 10 runs for each combination of sizes - the numbers seem quite
consistent and repeatable. I also looked at the median values.

dim 100k rows, fact 1M rows
---------------------------

master patched
------- -------
1 286.184 265.489
2 284.827 264.961
3 281.040 264.768
4 280.926 267.720
5 280.984 261.348
6 280.878 261.463
7 280.875 261.338
8 281.042 261.265
9 281.003 261.236
10 280.939 261.185
------- -------
med 280.994 261.406 (-7%)

dim 1M rows, fact 10M rows
--------------------------

master patched
-------- --------
1 4316.235 3690.373
2 4399.539 3738.097
3 4360.551 3655.602
4 4359.763 3626.142
5 4361.821 3621.941
6 4359.205 3654.835
7 4371.438 3631.212
8 4361.857 3626.237
9 4357.317 3676.651
10 4359.561 3641.830
-------- --------
med 4360.157 3648.333 (-17%)

dim 10M rows, fact 100M rows
----------------------------

master patched
-------- --------
1 46246.467 39561.597
2 45982.937 40083.352
3 45818.118 39674.661
4 45716.281 39616.585
5 45651.117 40463.966
6 45979.036 41395.390
7 46045.400 41358.047
8 45978.698 41253.946
9 45801.343 41156.440
10 45720.722 41374.688
--------- ---------
med 45898.408 40810.203 (-10%)

So the gains seem quite solid - it's not something that would make the
query an order of magnitude faster, but it's well above the noise.

Of course, in practice the queries will be more complicated, making the
improvement less significant, but I don't think that's a reason not to
apply it.

Two minor comments on the patch:

1) the 'subquery' variable in specialjoin_is_unique_join is unused

2) in the explain output, there should probably be a space before the
'(inner unique)' text, so

Hash Join (inner unique) ...

instead of

Hash Join(inner unique)

but that's just nitpicking at this point. Otherwise the patch seems
quite solid to me.

regard

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

Attachments:

unijoins-test.sqlapplication/sql; name=unijoins-test.sqlDownload
unijoins-queries.sqlapplication/sql; name=unijoins-queries.sqlDownload
#31David Rowley
david.rowley@2ndquadrant.com
In reply to: Tomas Vondra (#30)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 24 August 2015 at 07:31, Tomas Vondra <tomas.vondra@2ndquadrant.com>
wrote:

dim 100k rows, fact 1M rows
---------------------------

master patched
------- -------

..

med 280.994 261.406 (-7%)

dim 1M rows, fact 10M rows
--------------------------

master patched
-------- --------

..

med 4360.157 3648.333 (-17%)

dim 10M rows, fact 100M rows
----------------------------

master patched
-------- --------

..

med 45898.408 40810.203 (-10%)

So the gains seem quite solid - it's not something that would make the
query an order of magnitude faster, but it's well above the noise.

Of course, in practice the queries will be more complicated, making the
improvement less significant, but I don't think that's a reason not to
apply it.

Many thanks for doing that performance testing.

Two minor comments on the patch:

1) the 'subquery' variable in specialjoin_is_unique_join is unused

2) in the explain output, there should probably be a space before the
'(inner unique)' text, so

Hash Join (inner unique) ...

instead of

Hash Join(inner unique)

but that's just nitpicking at this point. Otherwise the patch seems quite
solid to me.

The attached fixes these two issues.

Regards

David Rowley

--
David Rowley http://www.2ndQuadrant.com/
<http://www.2ndquadrant.com/&gt;
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2015-08-24.patchapplication/octet-stream; name=unique_joins_2015-08-24.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 5d06fa4..304a473 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1179,9 +1179,17 @@ ExplainNode(PlanState *planstate, List *ancestors,
 						appendStringInfo(es->str, " %s Join", jointype);
 					else if (!IsA(plan, NestLoop))
 						appendStringInfoString(es->str, " Join");
+
+					if (((Join *) plan)->unique_inner)
+						appendStringInfoString(es->str, " (inner unique)");
+
 				}
 				else
+				{
 					ExplainPropertyText("Join Type", jointype, es);
+					ExplainPropertyText("Inner unique",
+							((Join *)plan)->unique_inner ? "true" : "false", es);
+				}
 			}
 			break;
 		case T_SetOp:
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 1d78cdf..f2471aa 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple. For the case of SEMI joins, we want to skip to
+					 * the next outer row after having matched 1 inner row.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.unique_inner)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -451,6 +453,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate = makeNode(HashJoinState);
 	hjstate->js.ps.plan = (Plan *) node;
 	hjstate->js.ps.state = estate;
+	hjstate->js.unique_inner = node->join.unique_inner;
 
 	/*
 	 * Miscellaneous initialization
@@ -498,8 +501,10 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			hjstate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 34b6cf6..100f085 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,12 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple. For the case of SEMI joins, we want to skip to
+					 * the next outer row after having matched 1 inner row.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.unique_inner)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1486,6 +1488,8 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.plan = (Plan *) node;
 	mergestate->js.ps.state = estate;
 
+	mergestate->js.unique_inner = node->join.unique_inner;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -1553,8 +1557,10 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			mergestate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e66bcda..b5e6de1 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,12 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * We'll consider returning the first match if the inner is
+			 * unique, but after that we're done with this outer tuple.
+			 * For the case of SEMI joins, we want to skip to the next outer
+			 * row after having matched 1 inner row.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.unique_inner)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -310,6 +312,8 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.plan = (Plan *) node;
 	nlstate->js.ps.state = estate;
 
+	nlstate->js.unique_inner = node->join.unique_inner;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -354,8 +358,10 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			nlstate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index bd2e80e..c72ed39 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2032,6 +2032,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(is_unique_join);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 19412fe..12f7787 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -837,6 +837,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(is_unique_join);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index a878498..13c83b3 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2015,6 +2015,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(is_unique_join);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 7069f60..2636bdb 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1783,7 +1783,7 @@ cost_group(Path *path, PlannerInfo *root,
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors)
@@ -1817,7 +1817,9 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI ||
+		unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -1888,7 +1890,9 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI ||
+		path->unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -2758,7 +2762,9 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		path->jpath.unique_inner)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index a35c881..0cf15d8 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -44,7 +45,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+										   RelOptInfo *innerrel);
 
 /*
  * add_paths_to_joinrel
@@ -81,12 +83,45 @@ add_paths_to_joinrel(PlannerInfo *root,
 	JoinPathExtraData extra;
 	bool		mergejoin_allowed = true;
 	ListCell   *lc;
+	bool		unique_inner;
+
+	/* left joins were already been analyzed for uniqueness in mark_unique_joins() */
+	if (jointype == JOIN_LEFT)
+		unique_inner = sjinfo->is_unique_join;
+	else if (jointype == JOIN_INNER &&
+		restrictlist != NIL &&
+		rel_supports_distinctness(root, innerrel))
+	{
+		/*
+		 * remember the number of items that were in the restrictlist as
+		 * the call to relation_has_unique_index_for may add more items
+		 * which we'll need to remove later.
+		 */
+		int org_len = list_length(restrictlist);
+
+		/*
+		 * rel_is_distinct_for requires restrict infos to have the
+		 * correct clause direction info
+		 */
+		foreach(lc, restrictlist)
+		{
+			clause_sides_match_join((RestrictInfo *)lfirst(lc),
+				outerrel, innerrel);
+		}
+		unique_inner = rel_is_distinct_for(root, innerrel, restrictlist);
+
+		/* Remove any list items added by rel_is_distinct_for */
+		list_truncate(restrictlist, org_len);
+	}
+	else
+		unique_inner = false;	/* we can't prove uniqueness */
 
 	extra.restrictlist = restrictlist;
 	extra.mergeclause_list = NIL;
 	extra.sjinfo = sjinfo;
 	extra.param_source_rels = NULL;
 	extra.extra_lateral_rels = NULL;
+	extra.unique_inner = unique_inner;
 
 	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
@@ -399,7 +434,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * The latter two steps are expensive enough to make this two-phase
 	 * methodology worthwhile.
 	 */
-	initial_cost_nestloop(root, &workspace, jointype,
+	initial_cost_nestloop(root, &workspace, jointype, extra->unique_inner,
 						  outer_path, inner_path,
 						  extra->sjinfo, &extra->semifactors);
 
@@ -411,6 +446,7 @@ try_nestloop_path(PlannerInfo *root,
 				 create_nestloop_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
@@ -494,6 +530,7 @@ try_mergejoin_path(PlannerInfo *root,
 				 create_mergejoin_path(root,
 									   joinrel,
 									   jointype,
+									   extra->unique_inner,
 									   &workspace,
 									   extra->sjinfo,
 									   outer_path,
@@ -565,6 +602,7 @@ try_hashjoin_path(PlannerInfo *root,
 				 create_hashjoin_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index b2cc9f0..51caeab 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -639,6 +639,7 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 		/* we don't bother trying to make the remaining fields valid */
 		sjinfo->lhs_strict = false;
 		sjinfo->delay_upper_joins = false;
+		sjinfo->is_unique_join = false;
 		sjinfo->semi_can_btree = false;
 		sjinfo->semi_can_hash = false;
 		sjinfo->semi_operators = NIL;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 7912b15..7f7c674 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -33,11 +33,37 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+/*
+ * mark_unique_joins
+		Analyze joins in order to determine if their inner side is unique based
+		on the join condition.
+ */
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs that have not already
+		 * been marked as unique by a previous call.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT &&
+			!sjinfo->is_unique_join &&
+			specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -91,6 +117,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to mark some joins as unique which we could
+		 * not do before
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -151,17 +183,17 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must not duplicate its outer side and must be a non-delaying left
+	 * join to a single baserel, else we aren't going to be able to do anything
+	 * with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->is_unique_join ||
+		sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -170,38 +202,7 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -212,7 +213,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -253,6 +255,44 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most 1 inner row for any single outer row. False is returned if there's
+ *		insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -274,10 +314,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we can't
+			 * mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -300,71 +338,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -562,6 +538,125 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list, callers should either
+ * make a copy of the list or trim it back to it's original length after
+ * calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *)lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *)rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *)get_rightop(rinfo->clause);
+			else
+				var = (Var *)get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false; /* can't prove rel to be distinct over clause_list */
+}
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's nothing to prove
+		 * uniqueness on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 404c6f5..fbe76b2 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -136,13 +136,12 @@ static BitmapAnd *make_bitmap_and(List *bitmapplans);
 static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
-			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  Plan *lefttree, Plan *righttree, JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -157,7 +156,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2313,7 +2312,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->path);
 
@@ -2607,7 +2606,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
@@ -2733,7 +2732,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3866,7 +3865,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3876,8 +3875,9 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 	node->nestParams = nestParams;
 
 	return node;
@@ -3890,7 +3890,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3901,8 +3901,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
@@ -3950,7 +3951,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3965,8 +3966,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 2f4e818..c5a5677 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1089,6 +1089,7 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	sjinfo->is_unique_join = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 848df97..55310d8 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -174,6 +174,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Analyze joins to find out which ones have a unique inner side */
+	mark_unique_joins(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 935bc2b..ab4389a 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1541,6 +1541,7 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  *
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_nestloop
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1556,6 +1557,7 @@ NestPath *
 create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1608,6 +1610,7 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->jointype = jointype;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
+	pathnode->unique_inner = unique_inner;
 	pathnode->joinrestrictinfo = restrict_clauses;
 
 	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
@@ -1622,6 +1625,7 @@ create_nestloop_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+  * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_mergejoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'outer_path' is the outer path
@@ -1638,6 +1642,7 @@ MergePath *
 create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -1665,6 +1670,7 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
@@ -1682,6 +1688,7 @@ create_mergejoin_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_hashjoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1696,6 +1703,7 @@ HashPath *
 create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1733,6 +1741,7 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5796de8..348ea3b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1642,6 +1642,7 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 0654d02..c11269e 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -591,6 +591,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 5dc23d9..77881cf 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1052,6 +1052,7 @@ typedef struct JoinPath
 
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
@@ -1428,6 +1429,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -1706,6 +1708,7 @@ typedef struct JoinPathExtraData
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
 	Relids		extra_lateral_rels;
+	bool		unique_inner;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index dd43e45..87c0d1e 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -115,7 +115,7 @@ extern void cost_group(Path *path, PlannerInfo *root,
 		   double input_tuples);
 extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 161644c..e9b547d 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -91,6 +91,7 @@ extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_pat
 extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -103,6 +104,7 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -117,6 +119,7 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 52b077a..d7d373b 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -124,7 +124,11 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+								List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 0391b8e..6e32a7e 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -186,7 +186,7 @@ explain (costs off)
   select * from ec1, ec2 where ff = x1 and x1 = '42'::int8alias2;
                QUERY PLAN                
 -----------------------------------------
- Nested Loop
+ Nested Loop (inner unique)
    ->  Seq Scan on ec2
          Filter: (x1 = '42'::int8alias2)
    ->  Index Scan using ec1_pkey on ec1
@@ -310,7 +310,7 @@ explain (costs off)
          ->  Index Scan using ec1_expr3 on ec1 ec1_5
          ->  Index Scan using ec1_expr4 on ec1 ec1_6
    ->  Materialize
-         ->  Merge Join
+         ->  Merge Join (inner unique)
                Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
                ->  Merge Append
                      Sort Key: (((ec1_1.ff + 2) + 1))
@@ -365,7 +365,7 @@ explain (costs off)
   where ss1.x = ec1.f1 and ec1.ff = 42::int8;
                      QUERY PLAN                      
 -----------------------------------------------------
- Merge Join
+ Merge Join (inner unique)
    Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
    ->  Merge Append
          Sort Key: (((ec1_1.ff + 2) + 1))
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 9d605a2..0a028ef 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2728,8 +2728,8 @@ from nt3 as nt3
 where nt3.id = 1 and ss2.b3;
                   QUERY PLAN                   
 -----------------------------------------------
- Nested Loop
-   ->  Nested Loop
+ Nested Loop (inner unique)
+   ->  Nested Loop (inner unique)
          ->  Index Scan using nt3_pkey on nt3
                Index Cond: (id = 1)
          ->  Index Scan using nt2_pkey on nt2
@@ -3816,7 +3816,7 @@ explain (costs off)
     on (p.k = ss.k);
            QUERY PLAN            
 ---------------------------------
- Hash Left Join
+ Hash Left Join (inner unique)
    Hash Cond: (p.k = c.k)
    ->  Seq Scan on parent p
    ->  Hash
@@ -5043,3 +5043,247 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join (inner unique)
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(9 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join (inner unique)
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join (inner unique)
+   Output: j1.id, j2.id
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(8 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join (inner unique)
+   Output: j1.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop (inner unique)
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(12 rows)
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop (inner unique)
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(12 rows)
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+         QUERY PLAN          
+-----------------------------
+ Hash Join
+   Output: j1.id, (1)
+   Hash Cond: (j1.id = (1))
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: (1)
+         ->  Result
+               Output: 1
+(9 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(7 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop (inner unique)
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 6fc80af..a79f497 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -276,7 +276,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle);
 EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
                      QUERY PLAN                     
 ----------------------------------------------------
- Nested Loop
+ Nested Loop (inner unique)
    ->  Subquery Scan on document
          Filter: f_leak(document.dtitle)
          ->  Seq Scan on document document_1
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 7f57526..a238a62 100644
--- a/src/test/regress/expected/select_views.out
+++ b/src/test/regress/expected/select_views.out
@@ -1411,7 +1411,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Join (inner unique)
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1432,7 +1432,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Join (inner unique)
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1466,7 +1466,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Join (inner unique)
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1497,7 +1497,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Join (inner unique)
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/expected/select_views_1.out b/src/test/regress/expected/select_views_1.out
index 5275ef0..5f21ac2 100644
--- a/src/test/regress/expected/select_views_1.out
+++ b/src/test/regress/expected/select_views_1.out
@@ -1411,7 +1411,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Join (inner unique)
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1432,7 +1432,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Join (inner unique)
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1466,7 +1466,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Join (inner unique)
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1497,7 +1497,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Join (inner unique)
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 528e1ef..bf94e9e 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1600,3 +1600,98 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#32Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#31)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

On 24 August 2015 at 07:31, Tomas Vondra <tomas.vondra@2ndquadrant.com>
wrote:

2) in the explain output, there should probably be a space before the
'(inner unique)' text, so

Hash Join (inner unique) ...

instead of

Hash Join(inner unique)

but that's just nitpicking at this point. Otherwise the patch seems quite
solid to me.

The attached fixes these two issues.

Please, no. Randomly sticking additional bits of information into Join
Type is a good way to render EXPLAIN output permanently unintelligible,
not only to humans but to whatever programs are still trying to read the
text output format (I think explain.depesz.com still does). It is also
not a great idea to put more semantic distance between the text and
non-text output representations.

I am not exactly convinced that this behavior needs to be visible in
EXPLAIN output at all, but if it does, it should be a separate field.

regards, tom lane

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

#33David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#32)
Re: Performance improvement for joins where outer side is unique

On 24 August 2015 at 12:19, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

On 24 August 2015 at 07:31, Tomas Vondra <tomas.vondra@2ndquadrant.com>
wrote:

2) in the explain output, there should probably be a space before the
'(inner unique)' text, so

Hash Join (inner unique) ...

instead of

Hash Join(inner unique)

but that's just nitpicking at this point. Otherwise the patch seems

quite

solid to me.

The attached fixes these two issues.

Please, no. Randomly sticking additional bits of information into Join
Type is a good way to render EXPLAIN output permanently unintelligible,
not only to humans but to whatever programs are still trying to read the
text output format (I think explain.depesz.com still does). It is also
not a great idea to put more semantic distance between the text and
non-text output representations.

I am not exactly convinced that this behavior needs to be visible in
EXPLAIN output at all, but if it does, it should be a separate field.

I have to admit I don't much like it either, originally I had this as an
extra property that was only seen in EXPLAIN VERBOSE.

    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Unique Join: No

There was a debate somewhere about this and it ended up the way it is now,
I didn't feel the need to argue for the EXPLAIN VERBOSR field as I thought
that a committer would likely change it anyway. However, if I remove all
changes to explain.c, then it makes it very difficult to write regression
tests which ensure the new code is doing what it's meant to.

What do you think of the extra EXPLAIN VERBOSE field?

Regards

David Rowley

--
David Rowley http://www.2ndQuadrant.com/
<http://www.2ndquadrant.com/&gt;
PostgreSQL Development, 24x7 Support, Training & Services

#34Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#33)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

On 24 August 2015 at 12:19, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I am not exactly convinced that this behavior needs to be visible in
EXPLAIN output at all, but if it does, it should be a separate field.

I have to admit I don't much like it either, originally I had this as an
extra property that was only seen in EXPLAIN VERBOSE.

Seems like a reasonable design from here. (Note that for non-text output,
I'd say the field should come out unconditionally. We only care about
abbreviating in text mode.)

regards, tom lane

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

#35David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#34)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 24 August 2015 at 14:29, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

I have to admit I don't much like it either, originally I had this as an
extra property that was only seen in EXPLAIN VERBOSE.

Seems like a reasonable design from here.

The attached patch has the format in this way.

(Note that for non-text output,
I'd say the field should come out unconditionally. We only care about

abbreviating in text mode.)

If that's the case then why do we not enable verbose for all of the
non-text outputs?
It seems strange to start making exceptions on a case-by-case basis.

Regards

David Rowley

--
David Rowley http://www.2ndQuadrant.com/
<http://www.2ndquadrant.com/&gt;
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2015-08-25_feb3068.patchapplication/octet-stream; name=unique_joins_2015-08-25_feb3068.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 5d06fa4..9a3208b 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1300,6 +1300,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *)plan)->unique_inner ? "Yes" : "No";
+				ExplainPropertyText("Unique Inner", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 1d78cdf..f2471aa 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple. For the case of SEMI joins, we want to skip to
+					 * the next outer row after having matched 1 inner row.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.unique_inner)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -451,6 +453,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate = makeNode(HashJoinState);
 	hjstate->js.ps.plan = (Plan *) node;
 	hjstate->js.ps.state = estate;
+	hjstate->js.unique_inner = node->join.unique_inner;
 
 	/*
 	 * Miscellaneous initialization
@@ -498,8 +501,10 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			hjstate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 34b6cf6..100f085 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,12 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * We'll consider returning the first match if the inner
+					 * is unique, but after that we're done with this outer
+					 * tuple. For the case of SEMI joins, we want to skip to
+					 * the next outer row after having matched 1 inner row.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.unique_inner)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1486,6 +1488,8 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.plan = (Plan *) node;
 	mergestate->js.ps.state = estate;
 
+	mergestate->js.unique_inner = node->join.unique_inner;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -1553,8 +1557,10 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			mergestate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e66bcda..b5e6de1 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,12 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * We'll consider returning the first match if the inner is
+			 * unique, but after that we're done with this outer tuple.
+			 * For the case of SEMI joins, we want to skip to the next outer
+			 * row after having matched 1 inner row.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.unique_inner)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -310,6 +312,8 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.plan = (Plan *) node;
 	nlstate->js.ps.state = estate;
 
+	nlstate->js.unique_inner = node->join.unique_inner;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -354,8 +358,10 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			nlstate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index bd2e80e..c72ed39 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2032,6 +2032,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(is_unique_join);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 19412fe..12f7787 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -837,6 +837,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(is_unique_join);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index a878498..13c83b3 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2015,6 +2015,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(is_unique_join);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 7069f60..2636bdb 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1783,7 +1783,7 @@ cost_group(Path *path, PlannerInfo *root,
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors)
@@ -1817,7 +1817,9 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI ||
+		unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -1888,7 +1890,9 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI ||
+		path->unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -2758,7 +2762,9 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		path->jpath.unique_inner)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index a35c881..0cf15d8 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -44,7 +45,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+										   RelOptInfo *innerrel);
 
 /*
  * add_paths_to_joinrel
@@ -81,12 +83,45 @@ add_paths_to_joinrel(PlannerInfo *root,
 	JoinPathExtraData extra;
 	bool		mergejoin_allowed = true;
 	ListCell   *lc;
+	bool		unique_inner;
+
+	/* left joins were already been analyzed for uniqueness in mark_unique_joins() */
+	if (jointype == JOIN_LEFT)
+		unique_inner = sjinfo->is_unique_join;
+	else if (jointype == JOIN_INNER &&
+		restrictlist != NIL &&
+		rel_supports_distinctness(root, innerrel))
+	{
+		/*
+		 * remember the number of items that were in the restrictlist as
+		 * the call to relation_has_unique_index_for may add more items
+		 * which we'll need to remove later.
+		 */
+		int org_len = list_length(restrictlist);
+
+		/*
+		 * rel_is_distinct_for requires restrict infos to have the
+		 * correct clause direction info
+		 */
+		foreach(lc, restrictlist)
+		{
+			clause_sides_match_join((RestrictInfo *)lfirst(lc),
+				outerrel, innerrel);
+		}
+		unique_inner = rel_is_distinct_for(root, innerrel, restrictlist);
+
+		/* Remove any list items added by rel_is_distinct_for */
+		list_truncate(restrictlist, org_len);
+	}
+	else
+		unique_inner = false;	/* we can't prove uniqueness */
 
 	extra.restrictlist = restrictlist;
 	extra.mergeclause_list = NIL;
 	extra.sjinfo = sjinfo;
 	extra.param_source_rels = NULL;
 	extra.extra_lateral_rels = NULL;
+	extra.unique_inner = unique_inner;
 
 	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
@@ -399,7 +434,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * The latter two steps are expensive enough to make this two-phase
 	 * methodology worthwhile.
 	 */
-	initial_cost_nestloop(root, &workspace, jointype,
+	initial_cost_nestloop(root, &workspace, jointype, extra->unique_inner,
 						  outer_path, inner_path,
 						  extra->sjinfo, &extra->semifactors);
 
@@ -411,6 +446,7 @@ try_nestloop_path(PlannerInfo *root,
 				 create_nestloop_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
@@ -494,6 +530,7 @@ try_mergejoin_path(PlannerInfo *root,
 				 create_mergejoin_path(root,
 									   joinrel,
 									   jointype,
+									   extra->unique_inner,
 									   &workspace,
 									   extra->sjinfo,
 									   outer_path,
@@ -565,6 +602,7 @@ try_hashjoin_path(PlannerInfo *root,
 				 create_hashjoin_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index b2cc9f0..51caeab 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -639,6 +639,7 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 		/* we don't bother trying to make the remaining fields valid */
 		sjinfo->lhs_strict = false;
 		sjinfo->delay_upper_joins = false;
+		sjinfo->is_unique_join = false;
 		sjinfo->semi_can_btree = false;
 		sjinfo->semi_can_hash = false;
 		sjinfo->semi_operators = NIL;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 7912b15..7f7c674 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -33,11 +33,37 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+/*
+ * mark_unique_joins
+		Analyze joins in order to determine if their inner side is unique based
+		on the join condition.
+ */
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs that have not already
+		 * been marked as unique by a previous call.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT &&
+			!sjinfo->is_unique_join &&
+			specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -91,6 +117,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to mark some joins as unique which we could
+		 * not do before
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -151,17 +183,17 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must not duplicate its outer side and must be a non-delaying left
+	 * join to a single baserel, else we aren't going to be able to do anything
+	 * with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->is_unique_join ||
+		sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -170,38 +202,7 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -212,7 +213,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -253,6 +255,44 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most 1 inner row for any single outer row. False is returned if there's
+ *		insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -274,10 +314,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we can't
+			 * mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -300,71 +338,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -562,6 +538,125 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list, callers should either
+ * make a copy of the list or trim it back to it's original length after
+ * calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *)lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *)rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *)get_rightop(rinfo->clause);
+			else
+				var = (Var *)get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false; /* can't prove rel to be distinct over clause_list */
+}
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's nothing to prove
+		 * uniqueness on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 404c6f5..fbe76b2 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -136,13 +136,12 @@ static BitmapAnd *make_bitmap_and(List *bitmapplans);
 static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
-			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  Plan *lefttree, Plan *righttree, JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -157,7 +156,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2313,7 +2312,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->path);
 
@@ -2607,7 +2606,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
@@ -2733,7 +2732,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_path_costsize(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3866,7 +3865,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3876,8 +3875,9 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 	node->nestParams = nestParams;
 
 	return node;
@@ -3890,7 +3890,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3901,8 +3901,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
@@ -3950,7 +3951,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3965,8 +3966,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 2f4e818..c5a5677 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1089,6 +1089,7 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	sjinfo->is_unique_join = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 848df97..55310d8 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -174,6 +174,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Analyze joins to find out which ones have a unique inner side */
+	mark_unique_joins(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 935bc2b..ab4389a 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1541,6 +1541,7 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  *
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_nestloop
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1556,6 +1557,7 @@ NestPath *
 create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1608,6 +1610,7 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->jointype = jointype;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
+	pathnode->unique_inner = unique_inner;
 	pathnode->joinrestrictinfo = restrict_clauses;
 
 	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
@@ -1622,6 +1625,7 @@ create_nestloop_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+  * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_mergejoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'outer_path' is the outer path
@@ -1638,6 +1642,7 @@ MergePath *
 create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -1665,6 +1670,7 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
@@ -1682,6 +1688,7 @@ create_mergejoin_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_hashjoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1696,6 +1703,7 @@ HashPath *
 create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1733,6 +1741,7 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5796de8..348ea3b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1642,6 +1642,7 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 0654d02..c11269e 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -591,6 +591,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 5dc23d9..77881cf 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1052,6 +1052,7 @@ typedef struct JoinPath
 
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
@@ -1428,6 +1429,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -1706,6 +1708,7 @@ typedef struct JoinPathExtraData
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
 	Relids		extra_lateral_rels;
+	bool		unique_inner;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index dd43e45..87c0d1e 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -115,7 +115,7 @@ extern void cost_group(Path *path, PlannerInfo *root,
 		   double input_tuples);
 extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 161644c..e9b547d 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -91,6 +91,7 @@ extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_pat
 extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -103,6 +104,7 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -117,6 +119,7 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 52b077a..d7d373b 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -124,7 +124,11 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+								List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 9d605a2..83f7f71 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3302,6 +3302,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Unique Inner: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3309,12 +3310,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Unique Inner: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3352,9 +3354,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3362,9 +3366,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3380,7 +3386,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3413,9 +3419,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3423,12 +3431,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Unique Inner: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3445,7 +3456,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3479,9 +3490,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3489,12 +3502,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Unique Inner: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3514,7 +3530,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3546,14 +3562,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Unique Inner: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Unique Inner: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Unique Inner: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3563,7 +3582,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3621,6 +3640,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Unique Inner: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3633,7 +3653,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -4519,12 +4539,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4551,12 +4572,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4584,6 +4606,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Unique Inner: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4591,7 +4614,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4611,12 +4634,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Unique Inner: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4638,10 +4662,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Unique Inner: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Unique Inner: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4650,7 +4676,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4696,10 +4722,12 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Unique Inner: No
          Join Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
@@ -4707,7 +4735,7 @@ select * from
                Output: c.q1
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1
-(13 rows)
+(15 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -4774,13 +4802,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Unique Inner: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Unique Inner: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Unique Inner: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Unique Inner: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -4796,7 +4828,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -4814,17 +4846,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Unique Inner: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Unique Inner: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Unique Inner: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Unique Inner: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Unique Inner: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -4846,7 +4883,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -4859,16 +4896,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Unique Inner: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Unique Inner: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -4881,10 +4920,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Unique Inner: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Unique Inner: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -4893,7 +4934,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -4920,10 +4961,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Unique Inner: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -4945,7 +4988,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5043,3 +5086,260 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Inner: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Unique Inner: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Inner: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Inner: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(13 rows)
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Inner: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(13 rows)
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+         QUERY PLAN          
+-----------------------------
+ Hash Join
+   Output: j1.id, (1)
+   Unique Inner: No
+   Hash Cond: (j1.id = (1))
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: (1)
+         ->  Result
+               Output: 1
+(10 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Unique Inner: No
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Inner: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Inner: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 6dabe50..39e97d7 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2016,12 +2016,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2042,11 +2043,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index de64ca7..b993eb2 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -783,6 +783,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Unique Inner: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -791,7 +792,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -811,6 +812,7 @@ select * from int4_tbl o where (f1, f1) in
 ----------------------------------------------------------------------
  Hash Join
    Output: o.f1
+   Unique Inner: No
    Hash Cond: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -827,7 +829,7 @@ select * from int4_tbl o where (f1, f1) in
                            Group Key: i.f1
                            ->  Seq Scan on public.int4_tbl i
                                  Output: i.f1
-(18 rows)
+(19 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 7eb9261..99e9f7c 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2136,6 +2136,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 3) AND leakproof(t1_5.a))
@@ -2153,6 +2154,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t11
                            Output: t11.ctid, t11.a, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 3) AND leakproof(t11.a))
@@ -2170,6 +2172,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 3) AND leakproof(t12_2.a))
@@ -2187,6 +2190,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 3) AND leakproof(t111_3.a))
@@ -2197,7 +2201,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 3)
-(73 rows)
+(77 rows)
 
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
 SELECT * FROM v1 WHERE a=100; -- Nothing should have been changed to 100
@@ -2226,6 +2230,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 8) AND leakproof(t1_5.a))
@@ -2243,6 +2248,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t11
                            Output: t11.a, t11.ctid, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 8) AND leakproof(t11.a))
@@ -2260,6 +2266,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 8) AND leakproof(t12_2.a))
@@ -2277,6 +2284,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 8) AND leakproof(t111_3.a))
@@ -2287,7 +2295,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 8)
-(73 rows)
+(77 rows)
 
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
 NOTICE:  snooped value: 8
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 2c9226c..02fb175 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2179,6 +2179,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2186,6 +2187,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2193,6 +2195,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2200,12 +2203,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 528e1ef..bf94e9e 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1600,3 +1600,98 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#36Michael Paquier
michael.paquier@gmail.com
In reply to: David Rowley (#35)
Re: Performance improvement for joins where outer side is unique

On Tue, Aug 25, 2015 at 2:25 PM, David Rowley
<david.rowley@2ndquadrant.com> wrote:

On 24 August 2015 at 14:29, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

I have to admit I don't much like it either, originally I had this as an
extra property that was only seen in EXPLAIN VERBOSE.

Seems like a reasonable design from here.

The attached patch has the format in this way.

(Note that for non-text output,
I'd say the field should come out unconditionally. We only care about

abbreviating in text mode.)

If that's the case then why do we not enable verbose for all of the non-text
outputs?
It seems strange to start making exceptions on a case-by-case basis.

Moved to CF 2015-09.
--
Michael

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

#37Robert Haas
robertmhaas@gmail.com
In reply to: David Rowley (#35)
Re: Performance improvement for joins where outer side is unique

On Tue, Aug 25, 2015 at 1:25 AM, David Rowley
<david.rowley@2ndquadrant.com> wrote:

If that's the case then why do we not enable verbose for all of the non-text
outputs?
It seems strange to start making exceptions on a case-by-case basis.

+1. FORMAT and VERBOSE are separate options, and each one should
control what the name suggests, not the other thing.

Also: very nice performance results.

--
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

#38David Rowley
david.rowley@2ndquadrant.com
In reply to: Robert Haas (#37)
Re: Performance improvement for joins where outer side is unique

On 4 September 2015 at 04:50, Robert Haas <robertmhaas@gmail.com> wrote:

Also: very nice performance results.

Thanks.

On following a thread in [General] [1]/messages/by-id/CACZYdDiAxEAM2RkMHBMuhwVcM4txH+5E3HQGgGyzfzbn-PnvFg@mail.gmail.com it occurred to me that this patch
can give a massive improvement on Merge joins where the mark and restore
causes an index scan to have to skip over many filtered rows again and
again.

I mocked up some tables and some data from the scenario on the [General]
thread:

create table customers (id bigint, group_id bigint not null);
insert into customers select x.x,x.x%27724+1 from generate_series(1,473733)
x(x);
alter table customers add constraint customer_pkey primary key (id);
create table balances (id bigint, balance int not null, tracking_number int
not null, customer_id bigint not null);
insert into balances select x.x, 100, 12345, x.x % 45 + 1 from
generate_Series(1,16876) x(x);
create index balance_customer_id_index on balances (customer_id);
create index balances_customer_tracking_number_index on balances
(customer_id,tracking_number);
analyze;

Unpatched I get:

test=# explain analyze SELECT ac.* FROM balances ac join customers o ON o.id
= ac.customer_id WHERE o.group_id = 45;

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------
Merge Join (cost=164.87..1868.70 rows=1 width=24) (actual
time=6.110..1491.408 rows=375 loops=1)
Merge Cond: (ac.customer_id = o.id)
-> Index Scan using balance_customer_id_index on balances ac
(cost=0.29..881.24 rows=16876 width=24) (actual time=0.009..5.206
rows=16876 loops=1)
-> Index Scan using customer_pkey on customers o (cost=0.42..16062.75
rows=17 width=8) (actual time=0.014..1484.382 rows=376 loops=1)
Filter: (group_id = 45)
Rows Removed by Filter: 10396168
Planning time: 0.207 ms
Execution time: 1491.469 ms
(8 rows)

Patched:

test=# explain analyze SELECT ac.* FROM balances ac join customers o ON o.id
= ac.customer_id WHERE o.group_id = 45;

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------
Merge Join (cost=164.87..1868.70 rows=1 width=24) (actual
time=6.037..11.528 rows=375 loops=1)
Merge Cond: (ac.customer_id = o.id)
-> Index Scan using balance_customer_id_index on balances ac
(cost=0.29..881.24 rows=16876 width=24) (actual time=0.009..4.978
rows=16876 loops=1)
-> Index Scan using customer_pkey on customers o (cost=0.42..16062.75
rows=17 width=8) (actual time=0.015..5.141 rows=2 loops=1)
Filter: (group_id = 45)
Rows Removed by Filter: 27766
Planning time: 0.204 ms
Execution time: 11.575 ms
(8 rows)

Now it could well be that the merge join costs need a bit more work to
avoid a merge join in this case, but as it stands as of today, this is your
performance gain.

Regards

David Rowley

[1]: /messages/by-id/CACZYdDiAxEAM2RkMHBMuhwVcM4txH+5E3HQGgGyzfzbn-PnvFg@mail.gmail.com
/messages/by-id/CACZYdDiAxEAM2RkMHBMuhwVcM4txH+5E3HQGgGyzfzbn-PnvFg@mail.gmail.com

--
David Rowley http://www.2ndQuadrant.com/
<http://www.2ndquadrant.com/&gt;
PostgreSQL Development, 24x7 Support, Training & Services

#39Pavel Stehule
pavel.stehule@gmail.com
In reply to: David Rowley (#38)
Re: Performance improvement for joins where outer side is unique

2015-10-13 23:28 GMT+02:00 David Rowley <david.rowley@2ndquadrant.com>:

On 4 September 2015 at 04:50, Robert Haas <robertmhaas@gmail.com> wrote:

Also: very nice performance results.

Thanks.

On following a thread in [General] [1] it occurred to me that this patch
can give a massive improvement on Merge joins where the mark and restore
causes an index scan to have to skip over many filtered rows again and
again.

I mocked up some tables and some data from the scenario on the [General]
thread:

create table customers (id bigint, group_id bigint not null);
insert into customers select x.x,x.x%27724+1 from
generate_series(1,473733) x(x);
alter table customers add constraint customer_pkey primary key (id);
create table balances (id bigint, balance int not null, tracking_number
int not null, customer_id bigint not null);
insert into balances select x.x, 100, 12345, x.x % 45 + 1 from
generate_Series(1,16876) x(x);
create index balance_customer_id_index on balances (customer_id);
create index balances_customer_tracking_number_index on balances
(customer_id,tracking_number);
analyze;

Unpatched I get:

test=# explain analyze SELECT ac.* FROM balances ac join customers o ON
o.id = ac.customer_id WHERE o.group_id = 45;

QUERY PLAN

--------------------------------------------------------------------------------------------------------------------------------------------------------
Merge Join (cost=164.87..1868.70 rows=1 width=24) (actual
time=6.110..1491.408 rows=375 loops=1)
Merge Cond: (ac.customer_id = o.id)
-> Index Scan using balance_customer_id_index on balances ac
(cost=0.29..881.24 rows=16876 width=24) (actual time=0.009..5.206
rows=16876 loops=1)
-> Index Scan using customer_pkey on customers o (cost=0.42..16062.75
rows=17 width=8) (actual time=0.014..1484.382 rows=376 loops=1)
Filter: (group_id = 45)
Rows Removed by Filter: 10396168
Planning time: 0.207 ms
Execution time: 1491.469 ms
(8 rows)

Patched:

test=# explain analyze SELECT ac.* FROM balances ac join customers o ON
o.id = ac.customer_id WHERE o.group_id = 45;

QUERY PLAN

--------------------------------------------------------------------------------------------------------------------------------------------------------
Merge Join (cost=164.87..1868.70 rows=1 width=24) (actual
time=6.037..11.528 rows=375 loops=1)
Merge Cond: (ac.customer_id = o.id)
-> Index Scan using balance_customer_id_index on balances ac
(cost=0.29..881.24 rows=16876 width=24) (actual time=0.009..4.978
rows=16876 loops=1)
-> Index Scan using customer_pkey on customers o (cost=0.42..16062.75
rows=17 width=8) (actual time=0.015..5.141 rows=2 loops=1)
Filter: (group_id = 45)
Rows Removed by Filter: 27766
Planning time: 0.204 ms
Execution time: 11.575 ms
(8 rows)

Now it could well be that the merge join costs need a bit more work to
avoid a merge join in this case, but as it stands as of today, this is your
performance gain.

Regards

it is great

Pavel

Show quoted text

David Rowley

[1]
/messages/by-id/CACZYdDiAxEAM2RkMHBMuhwVcM4txH+5E3HQGgGyzfzbn-PnvFg@mail.gmail.com

--
David Rowley http://www.2ndQuadrant.com/
<http://www.2ndquadrant.com/&gt;
PostgreSQL Development, 24x7 Support, Training & Services

#40Robert Haas
robertmhaas@gmail.com
In reply to: Pavel Stehule (#39)
Re: Performance improvement for joins where outer side is unique

On Wed, Oct 14, 2015 at 1:03 AM, Pavel Stehule <pavel.stehule@gmail.com> wrote:

it is great

+1.

--
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

#41David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#35)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 25 August 2015 at 17:25, David Rowley <david.rowley@2ndquadrant.com>
wrote:

On 24 August 2015 at 14:29, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

I have to admit I don't much like it either, originally I had this as an
extra property that was only seen in EXPLAIN VERBOSE.

Seems like a reasonable design from here.

The attached patch has the format in this way.

I've attached a rebased patch against current master as there were some
conflicts from the recent changes to LATERAL join.

On reviewing the patch again I was reminded that the bulk of the changes in
the patch are in analyzejoins.c. These are mostly just a refactor of the
current code to make it more reusable. The diff looks a bit more scary than
it actually is.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_16-12-2015_7a290ab1.patchapplication/octet-stream; name=unique_joins_16-12-2015_7a290ab1.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 12dae77..d1a9729 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1221,6 +1221,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *)plan)->unique_inner ? "Yes" : "No";
+				ExplainPropertyText("Unique Inner", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 1d78cdf..4eae85f 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,11 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * When the inner side is unique or we're performing a
+					 * semijoin, we'll consider returning the first match, but
+					 * after that we're done with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.unique_inner)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -451,6 +452,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate = makeNode(HashJoinState);
 	hjstate->js.ps.plan = (Plan *) node;
 	hjstate->js.ps.state = estate;
+	hjstate->js.unique_inner = node->join.unique_inner;
 
 	/*
 	 * Miscellaneous initialization
@@ -498,8 +500,10 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			hjstate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 34b6cf6..2cd532d 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,11 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * When the inner side is unique or we're performing a
+					 * semijoin, we'll consider returning the first match, but
+					 * after that we're done with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.unique_inner)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1486,6 +1487,8 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.plan = (Plan *) node;
 	mergestate->js.ps.state = estate;
 
+	mergestate->js.unique_inner = node->join.unique_inner;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -1553,8 +1556,10 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			mergestate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e66bcda..6f0297d 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,11 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * When the inner side is unique or we're performing a semijoin,
+			 * we'll consider returning the first match, but after that we're
+			 * done with this outer tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.unique_inner)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -310,6 +311,8 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.plan = (Plan *) node;
 	nlstate->js.ps.state = estate;
 
+	nlstate->js.unique_inner = node->join.unique_inner;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -354,8 +357,10 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			nlstate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index ba04b72..421f51e 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2058,6 +2058,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(is_unique_join);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 356fcaf..596bea7 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -837,6 +837,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(is_unique_join);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 63fae82..54b01fa 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2050,6 +2050,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(is_unique_join);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 990486c..35cec95 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1834,7 +1834,7 @@ cost_group(Path *path, PlannerInfo *root,
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors)
@@ -1868,7 +1868,9 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI ||
+		unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -1939,7 +1941,9 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI ||
+		path->unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -2809,7 +2813,9 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		path->jpath.unique_inner)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 53d8fdd..d74e496 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -44,7 +45,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+										   RelOptInfo *innerrel);
 
 /*
  * add_paths_to_joinrel
@@ -81,11 +83,44 @@ add_paths_to_joinrel(PlannerInfo *root,
 	JoinPathExtraData extra;
 	bool		mergejoin_allowed = true;
 	ListCell   *lc;
+	bool		unique_inner;
+
+	/* left joins were already been analyzed for uniqueness in mark_unique_joins() */
+	if (jointype == JOIN_LEFT)
+		unique_inner = sjinfo->is_unique_join;
+	else if (jointype == JOIN_INNER &&
+		restrictlist != NIL &&
+		rel_supports_distinctness(root, innerrel))
+	{
+		/*
+		 * remember the number of items that were in the restrictlist as
+		 * the call to relation_has_unique_index_for may add more items
+		 * which we'll need to remove later.
+		 */
+		int org_len = list_length(restrictlist);
+
+		/*
+		 * rel_is_distinct_for requires restrict infos to have the
+		 * correct clause direction info
+		 */
+		foreach(lc, restrictlist)
+		{
+			clause_sides_match_join((RestrictInfo *)lfirst(lc),
+				outerrel, innerrel);
+		}
+		unique_inner = rel_is_distinct_for(root, innerrel, restrictlist);
+
+		/* Remove any list items added by rel_is_distinct_for */
+		list_truncate(restrictlist, org_len);
+	}
+	else
+		unique_inner = false;	/* we can't prove uniqueness */
 
 	extra.restrictlist = restrictlist;
 	extra.mergeclause_list = NIL;
 	extra.sjinfo = sjinfo;
 	extra.param_source_rels = NULL;
+	extra.unique_inner = unique_inner;
 
 	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
@@ -301,7 +336,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * The latter two steps are expensive enough to make this two-phase
 	 * methodology worthwhile.
 	 */
-	initial_cost_nestloop(root, &workspace, jointype,
+	initial_cost_nestloop(root, &workspace, jointype, extra->unique_inner,
 						  outer_path, inner_path,
 						  extra->sjinfo, &extra->semifactors);
 
@@ -313,6 +348,7 @@ try_nestloop_path(PlannerInfo *root,
 				 create_nestloop_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
@@ -390,6 +426,7 @@ try_mergejoin_path(PlannerInfo *root,
 				 create_mergejoin_path(root,
 									   joinrel,
 									   jointype,
+									   extra->unique_inner,
 									   &workspace,
 									   extra->sjinfo,
 									   outer_path,
@@ -455,6 +492,7 @@ try_hashjoin_path(PlannerInfo *root,
 				 create_hashjoin_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index ad58058..d6a9161 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -701,6 +701,7 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 		/* we don't bother trying to make the remaining fields valid */
 		sjinfo->lhs_strict = false;
 		sjinfo->delay_upper_joins = false;
+		sjinfo->is_unique_join = false;
 		sjinfo->semi_can_btree = false;
 		sjinfo->semi_can_hash = false;
 		sjinfo->semi_operators = NIL;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index d188d97..f583477 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -33,11 +33,37 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+/*
+ * mark_unique_joins
+		Analyze joins in order to determine if their inner side is unique based
+		on the join condition.
+ */
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs that have not already
+		 * been marked as unique by a previous call.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT &&
+			!sjinfo->is_unique_join &&
+			specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -91,6 +117,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to mark some joins as unique which we could
+		 * not do before
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -151,17 +183,17 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must not duplicate its outer side and must be a non-delaying left
+	 * join to a single baserel, else we aren't going to be able to do anything
+	 * with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->is_unique_join ||
+		sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -170,38 +202,7 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -212,7 +213,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -253,6 +255,44 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most 1 inner row for any single outer row. False is returned if there's
+ *		insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -274,10 +314,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we can't
+			 * mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -300,71 +338,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -559,6 +535,125 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list, callers should either
+ * make a copy of the list or trim it back to it's original length after
+ * calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *)lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *)rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *)get_rightop(rinfo->clause);
+			else
+				var = (Var *)get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false; /* can't prove rel to be distinct over clause_list */
+}
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's nothing to prove
+		 * uniqueness on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 32f903d..20830db 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -140,13 +140,12 @@ static BitmapAnd *make_bitmap_and(List *bitmapplans);
 static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
-			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  Plan *lefttree, Plan *righttree, JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -161,7 +160,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2359,7 +2358,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -2653,7 +2652,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -2779,7 +2778,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3919,7 +3918,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3929,8 +3928,9 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 	node->nestParams = nestParams;
 
 	return node;
@@ -3943,7 +3943,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3954,8 +3954,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
@@ -4003,7 +4004,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -4018,8 +4019,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index f2dce6a..4dac6e8 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1110,6 +1110,7 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	sjinfo->is_unique_join = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 894f968..1e723a9 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -185,6 +185,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Analyze joins to find out which ones have a unique inner side */
+	mark_unique_joins(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index ec0910d..e164026 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1589,6 +1589,7 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  *
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_nestloop
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1604,6 +1605,7 @@ NestPath *
 create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1657,6 +1659,7 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->jointype = jointype;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
+	pathnode->unique_inner = unique_inner;
 	pathnode->joinrestrictinfo = restrict_clauses;
 
 	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
@@ -1671,6 +1674,7 @@ create_nestloop_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+  * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_mergejoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'outer_path' is the outer path
@@ -1687,6 +1691,7 @@ MergePath *
 create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -1715,6 +1720,7 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
@@ -1732,6 +1738,7 @@ create_mergejoin_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_hashjoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1746,6 +1753,7 @@ HashPath *
 create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1784,6 +1792,7 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5ccf470..d0e2b89 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1650,6 +1650,7 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 37086c6..76b7900 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -604,6 +604,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 5393005..66a0952 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1078,6 +1078,7 @@ typedef struct JoinPath
 
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
@@ -1454,6 +1455,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -1685,6 +1687,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * unique_inner is set to True if the planner has determined that the inner
+ *		side of the join can at most produce one tuple for each outer tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -1693,6 +1697,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		unique_inner;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index ac21a3a..bb7f176 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -120,7 +120,7 @@ extern void cost_group(Path *path, PlannerInfo *root,
 		   double input_tuples);
 extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 8fb9eda..46982fb 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -95,6 +95,7 @@ extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_pat
 extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -107,6 +108,7 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -121,6 +123,7 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index f96e9ee..0d91157 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -125,7 +125,11 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+								List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 64f046e..5662bf6 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3302,6 +3302,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Unique Inner: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3309,12 +3310,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Unique Inner: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3352,9 +3354,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3362,9 +3366,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3380,7 +3386,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3413,9 +3419,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3423,12 +3431,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Unique Inner: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3445,7 +3456,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3479,9 +3490,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3489,12 +3502,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Unique Inner: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3514,7 +3530,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3546,14 +3562,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Unique Inner: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Unique Inner: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Unique Inner: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3563,7 +3582,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3593,9 +3612,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Unique Inner: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Unique Inner: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3607,7 +3628,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3632,11 +3653,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Unique Inner: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Unique Inner: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Unique Inner: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3652,7 +3676,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3678,10 +3702,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Unique Inner: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Unique Inner: No
          ->  Nested Loop
                Output: tt1.f1
+               Unique Inner: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3691,6 +3718,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Unique Inner: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3705,7 +3733,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3735,13 +3763,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Unique Inner: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Unique Inner: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Unique Inner: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Unique Inner: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3758,7 +3790,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3815,6 +3847,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Unique Inner: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3827,7 +3860,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -4713,12 +4746,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4745,12 +4779,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4778,6 +4813,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Unique Inner: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4785,7 +4821,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4805,12 +4841,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Unique Inner: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4832,10 +4869,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Unique Inner: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Unique Inner: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4844,7 +4883,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4890,16 +4929,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Unique Inner: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -4966,13 +5007,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Unique Inner: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Unique Inner: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Unique Inner: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Unique Inner: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -4988,7 +5033,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5006,17 +5051,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Unique Inner: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Unique Inner: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Unique Inner: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Unique Inner: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Unique Inner: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5038,7 +5088,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5051,16 +5101,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Unique Inner: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Unique Inner: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5073,10 +5125,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Unique Inner: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Unique Inner: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5085,7 +5139,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5112,10 +5166,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Unique Inner: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5137,7 +5193,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5235,3 +5291,260 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Inner: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Unique Inner: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Inner: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Inner: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(13 rows)
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Inner: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(13 rows)
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+         QUERY PLAN          
+-----------------------------
+ Hash Join
+   Output: j1.id, (1)
+   Unique Inner: No
+   Hash Cond: (j1.id = (1))
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: (1)
+         ->  Result
+               Output: 1
+(10 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Unique Inner: No
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Inner: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Inner: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 00ef421..83f0b7a 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2012,12 +2012,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2038,11 +2039,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index de64ca7..b993eb2 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -783,6 +783,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Unique Inner: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -791,7 +792,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -811,6 +812,7 @@ select * from int4_tbl o where (f1, f1) in
 ----------------------------------------------------------------------
  Hash Join
    Output: o.f1
+   Unique Inner: No
    Hash Cond: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -827,7 +829,7 @@ select * from int4_tbl o where (f1, f1) in
                            Group Key: i.f1
                            ->  Seq Scan on public.int4_tbl i
                                  Output: i.f1
-(18 rows)
+(19 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 8e5463a..8967cf0 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2136,6 +2136,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 3) AND leakproof(t1_5.a))
@@ -2153,6 +2154,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t11
                            Output: t11.ctid, t11.a, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 3) AND leakproof(t11.a))
@@ -2170,6 +2172,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 3) AND leakproof(t12_2.a))
@@ -2187,6 +2190,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 3) AND leakproof(t111_3.a))
@@ -2197,7 +2201,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 3)
-(73 rows)
+(77 rows)
 
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
 SELECT * FROM v1 WHERE a=100; -- Nothing should have been changed to 100
@@ -2226,6 +2230,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 8) AND leakproof(t1_5.a))
@@ -2243,6 +2248,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t11
                            Output: t11.a, t11.ctid, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 8) AND leakproof(t11.a))
@@ -2260,6 +2266,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 8) AND leakproof(t12_2.a))
@@ -2277,6 +2284,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 8) AND leakproof(t111_3.a))
@@ -2287,7 +2295,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 8)
-(73 rows)
+(77 rows)
 
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
 NOTICE:  snooped value: 8
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 137420d..c64d433 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2183,6 +2183,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2190,6 +2191,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2197,6 +2199,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2204,12 +2207,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 0358d00..261ef90 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1678,3 +1678,98 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#42Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: David Rowley (#41)
Re: Performance improvement for joins where outer side is unique

Hi,

On 12/16/2015 01:27 AM, David Rowley wrote:

I've attached a rebased patch against current master as there were
some conflicts from the recent changes to LATERAL join.

Thanks. I've looked at the rebased patch and have a few minor comments.

0) I know the patch does not tweak costing - any plans in this
direction? Would it be possible to simply use the costing used by
semijoin?

1) nodeHashjoin.c (and other join nodes)

I've noticed we have this in the ExecHashJoin() method:

/*
* When the inner side is unique or we're performing a
* semijoin, we'll consider returning the first match, but
* after that we're done with this outer tuple.
*/
if (node->js.unique_inner)
node->hj_JoinState = HJ_NEED_NEW_OUTER;

That seems a bit awkward because the comment speaks about unique joins
*OR* semijoins, but the check only references unique_inner. That of
course works because we do this in ExecInitHashJoin():

switch (node->join.jointype)
{
case JOIN_SEMI:
hjstate->js.unique_inner = true;
/* fall through */

Either some semijoins may not be unique - in that case the naming is
misleading at best, and we either need to find a better name or simply
check two conditions like this:

if (node->js.unique_inner || node->join.type == JOIN_SEMI)
node->hj_JoinState = HJ_NEED_NEW_OUTER;

Or all semijoins are unique joins, and in that case the comment may need
rephrasing. But more importantly, it begs the question why we're
detecting this in the executor and not in the planner? Because if we
detect it in executor, we can't use this bit in costing, for example.

FWIW the misleading naming was actually mentioned in the thread by
Horiguchi-san, but I don't see any response to that (might have missed
it though).

2) analyzejoins.c

I see that this code in join_is_removable()

innerrel = find_base_rel(root, innerrelid);

if (innerrel->reloptkind != RELOPT_BASEREL)
return false;

was rewritten like this:

innerrel = find_base_rel(root, innerrelid);

Assert(innerrel->reloptkind == RELOPT_BASEREL);

That suggests that previously the function expected cases where
reloptkind may not be RELOPT_BASEREL, but now it'll error out int such
cases. I haven't noticed any changes in the surrounding code that'd
guarantee this won't happen, but I also haven't been able to come up
with an example triggering the assert (haven't been trying too hard).
How do we know the assert() makes sense?

3) joinpath.c

- either "were" or "been" seems redundant

/* left joins were already been analyzed for uniqueness in
mark_unique_joins() */

4) analyzejoins.c

comment format broken

/*
* mark_unique_joins
Analyze joins in order to determine if their inner side is unique based
on the join condition.
*/

5) analyzejoins.c

missing line before the comment

}
/*
* rel_supports_distinctness
* Returns true if rel has some properties which can prove the relation
* to be unique over some set of columns.
*

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

#43David Rowley
david.rowley@2ndquadrant.com
In reply to: Tomas Vondra (#42)
2 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 17 December 2015 at 05:02, Tomas Vondra <tomas.vondra@2ndquadrant.com>
wrote:

0) I know the patch does not tweak costing - any plans in this

direction? Would it be possible to simply use the costing used by

semijoin?

Many thanks for looking at this.

The patch does tweak the costings so that unique joins are costed in the
same way as semi joins. It's a very small change, so easy to miss.

For example, see the change in initial_cost_nestloop()

-       if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+       if (jointype == JOIN_SEMI ||
+               jointype == JOIN_ANTI ||
+               unique_inner)
        {

Also see the changes in final_cost_nestloop() and final_cost_hashjoin()

1) nodeHashjoin.c (and other join nodes)

I've noticed we have this in the ExecHashJoin() method:

/*
* When the inner side is unique or we're performing a
* semijoin, we'll consider returning the first match, but
* after that we're done with this outer tuple.
*/
if (node->js.unique_inner)
node->hj_JoinState = HJ_NEED_NEW_OUTER;

That seems a bit awkward because the comment speaks about unique joins
*OR* semijoins, but the check only references unique_inner. That of course
works because we do this in ExecInitHashJoin():

switch (node->join.jointype)
{
case JOIN_SEMI:
hjstate->js.unique_inner = true;
/* fall through */

Either some semijoins may not be unique - in that case the naming is
misleading at best, and we either need to find a better name or simply
check two conditions like this:

if (node->js.unique_inner || node->join.type == JOIN_SEMI)
node->hj_JoinState = HJ_NEED_NEW_OUTER;

Or all semijoins are unique joins, and in that case the comment may need
rephrasing. But more importantly, it begs the question why we're detecting
this in the executor and not in the planner? Because if we detect it in
executor, we can't use this bit in costing, for example.

The reason not to detect that in the planner is simply that unique_join is
meant to mean that the relation on the inner side of the join will at most
contain a single Tuple which matches each outer relation tuple, based on
the join condition. I've added no detection for this in semi joins, and I'd
rather not go blindly setting the flag to true in the planner as it simply
may not be true for the semi join. At the moment that might not matter as
we're only using the unique_join flag as an optimization in the join nodes,
but I'd prefer not to do this as its likely we'll want to do more with this
flag later, and I'd rather we keep the meaning well defined. You might
argue that this is also true for semi joins, but if down the road somewhere
we want to perform some checks on the inner relation before the join takes
place, and in that case the Tuples of the relation might not have the same
properties we claim they do.

But you're right that reusing the flag in the join nodes is not ideal, and
the comment is not that great either. I'd really rather go down the path of
either renaming the variable, or explaining this better in the comment. It
seems unnecessary to check both for each tuple being joined. I'd imagine
that might add a little overhead to joins which are not unique.

How about changing the comment to:

/*
* In the event that the inner side has been detected to be
* unique, as an optimization we can skip searching for any
* subsequent matching inner tuples, as none will exist.
* For semijoins unique_inner will always be true, although
* in this case we don't match another inner tuple as this
* is the required semi join behavior.
*/

Alternatively or additionally we can rename the variable in the executor
state, although I've not thought of a name yet that I don't find overly
verbose: unique_inner_or_semi_join, match_first_tuple_only.

2) analyzejoins.c

I see that this code in join_is_removable()

innerrel = find_base_rel(root, innerrelid);

if (innerrel->reloptkind != RELOPT_BASEREL)
return false;

was rewritten like this:

innerrel = find_base_rel(root, innerrelid);

Assert(innerrel->reloptkind == RELOPT_BASEREL);

That suggests that previously the function expected cases where reloptkind
may not be RELOPT_BASEREL, but now it'll error out int such cases. I
haven't noticed any changes in the surrounding code that'd guarantee this
won't happen, but I also haven't been able to come up with an example
triggering the assert (haven't been trying too hard). How do we know the
assert() makes sense?

I'd have changed this as this should be covered by the if
(!sjinfo->is_unique_join || a few lines up. mark_unique_joins() only sets
is_unique_join to true if specialjoin_is_unique_join() returns true and
that function tests reloptkind to ensure its set to RELOPT_BASEREL and
return false if the relation is not. Perhaps what is missing is a comment
to explain the function should only be used on RELOPT_BASEREL type
relations.

3) joinpath.c

- either "were" or "been" seems redundant

/* left joins were already been analyzed for uniqueness in
mark_unique_joins() */

Good catch. Fixed

4) analyzejoins.c

comment format broken

/*
* mark_unique_joins
Analyze joins in order to determine if their inner side is unique
based
on the join condition.
*/

Fixed

5) analyzejoins.c

missing line before the comment

}
/*
* rel_supports_distinctness
* Returns true if rel has some properties which can prove
the relation
* to be unique over some set of columns.
*

Fixed

I've attached updated patches.

Thanks again for the review!

--
David Rowley http://www.2ndQuadrant.com/
<http://www.2ndquadrant.com/&gt;
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_17-12-2015_d7b399e.patchapplication/octet-stream; name=unique_joins_17-12-2015_d7b399e.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 12dae77..d1a9729 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1221,6 +1221,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *)plan)->unique_inner ? "Yes" : "No";
+				ExplainPropertyText("Unique Inner", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 1d78cdf..ef588d1 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,14 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In the event that the inner side has been detected to be
+					 * unique, as an optimization we can skip searching for any
+					 * subsequent matching inner tuples, as none will exist.
+					 * For semijoins unique_inner will always be true, although
+					 * in this case we don't match another inner tuple as this
+					 * is the required semi join behavior.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.unique_inner)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -451,6 +455,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate = makeNode(HashJoinState);
 	hjstate->js.ps.plan = (Plan *) node;
 	hjstate->js.ps.state = estate;
+	hjstate->js.unique_inner = node->join.unique_inner;
 
 	/*
 	 * Miscellaneous initialization
@@ -498,8 +503,10 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			hjstate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 34b6cf6..0445d66 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,14 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In the event that the inner side has been detected to be
+					 * unique, as an optimization we can skip searching for any
+					 * subsequent matching inner tuples, as none will exist.
+					 * For semijoins unique_inner will always be true, although
+					 * in this case we don't match another inner tuple as this
+					 * is the required semi join behavior.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.unique_inner)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1486,6 +1490,8 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.plan = (Plan *) node;
 	mergestate->js.ps.state = estate;
 
+	mergestate->js.unique_inner = node->join.unique_inner;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -1553,8 +1559,10 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			mergestate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e66bcda..cfb9c9d 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,14 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * In the event that the inner side has been detected to be unique,
+			 * as an optimization we can skip searching for any subsequent
+			 * matching inner tuples, as none will exist. For semijoins
+			 * unique_inner will always be true, although in this case we don't
+			 * match another inner tuple as this is the required semi join
+			 * behavior.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.unique_inner)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -310,6 +314,8 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.plan = (Plan *) node;
 	nlstate->js.ps.state = estate;
 
+	nlstate->js.unique_inner = node->join.unique_inner;
+
 	/*
 	 * Miscellaneous initialization
 	 *
@@ -354,8 +360,10 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			nlstate->js.unique_inner = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index ba04b72..421f51e 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2058,6 +2058,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(is_unique_join);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 356fcaf..596bea7 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -837,6 +837,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(is_unique_join);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 63fae82..54b01fa 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2050,6 +2050,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(is_unique_join);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 990486c..35cec95 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1834,7 +1834,7 @@ cost_group(Path *path, PlannerInfo *root,
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors)
@@ -1868,7 +1868,9 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI ||
+		unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -1939,7 +1941,9 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI ||
+		path->unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -2809,7 +2813,9 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		path->jpath.unique_inner)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 53d8fdd..13e31a4 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -44,7 +45,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+										   RelOptInfo *innerrel);
 
 /*
  * add_paths_to_joinrel
@@ -81,11 +83,44 @@ add_paths_to_joinrel(PlannerInfo *root,
 	JoinPathExtraData extra;
 	bool		mergejoin_allowed = true;
 	ListCell   *lc;
+	bool		unique_inner;
+
+	/* left joins were already analyzed for uniqueness in mark_unique_joins() */
+	if (jointype == JOIN_LEFT)
+		unique_inner = sjinfo->is_unique_join;
+	else if (jointype == JOIN_INNER &&
+		restrictlist != NIL &&
+		rel_supports_distinctness(root, innerrel))
+	{
+		/*
+		 * remember the number of items that were in the restrictlist as
+		 * the call to relation_has_unique_index_for may add more items
+		 * which we'll need to remove later.
+		 */
+		int org_len = list_length(restrictlist);
+
+		/*
+		 * rel_is_distinct_for requires restrict infos to have the
+		 * correct clause direction info
+		 */
+		foreach(lc, restrictlist)
+		{
+			clause_sides_match_join((RestrictInfo *)lfirst(lc),
+				outerrel, innerrel);
+		}
+		unique_inner = rel_is_distinct_for(root, innerrel, restrictlist);
+
+		/* Remove any list items added by rel_is_distinct_for */
+		list_truncate(restrictlist, org_len);
+	}
+	else
+		unique_inner = false;	/* we can't prove uniqueness */
 
 	extra.restrictlist = restrictlist;
 	extra.mergeclause_list = NIL;
 	extra.sjinfo = sjinfo;
 	extra.param_source_rels = NULL;
+	extra.unique_inner = unique_inner;
 
 	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
@@ -301,7 +336,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * The latter two steps are expensive enough to make this two-phase
 	 * methodology worthwhile.
 	 */
-	initial_cost_nestloop(root, &workspace, jointype,
+	initial_cost_nestloop(root, &workspace, jointype, extra->unique_inner,
 						  outer_path, inner_path,
 						  extra->sjinfo, &extra->semifactors);
 
@@ -313,6 +348,7 @@ try_nestloop_path(PlannerInfo *root,
 				 create_nestloop_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
@@ -390,6 +426,7 @@ try_mergejoin_path(PlannerInfo *root,
 				 create_mergejoin_path(root,
 									   joinrel,
 									   jointype,
+									   extra->unique_inner,
 									   &workspace,
 									   extra->sjinfo,
 									   outer_path,
@@ -455,6 +492,7 @@ try_hashjoin_path(PlannerInfo *root,
 				 create_hashjoin_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index ad58058..d6a9161 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -701,6 +701,7 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 		/* we don't bother trying to make the remaining fields valid */
 		sjinfo->lhs_strict = false;
 		sjinfo->delay_upper_joins = false;
+		sjinfo->is_unique_join = false;
 		sjinfo->semi_can_btree = false;
 		sjinfo->semi_can_hash = false;
 		sjinfo->semi_operators = NIL;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index d188d97..2262183 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -33,11 +33,37 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+/*
+ * mark_unique_joins
+ *		Analyze joins in order to determine if their inner side is unique based
+ *		on the join condition.
+ */
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs that have not already
+		 * been marked as unique by a previous call.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT &&
+			!sjinfo->is_unique_join &&
+			specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -91,6 +117,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to mark some joins as unique which we could
+		 * not do before
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -151,17 +183,17 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must not duplicate its outer side and must be a non-delaying left
+	 * join to a single baserel, else we aren't going to be able to do anything
+	 * with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->is_unique_join ||
+		sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -170,38 +202,7 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -212,7 +213,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -253,6 +255,44 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most 1 inner row for any single outer row. False is returned if there's
+ *		insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -274,10 +314,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we can't
+			 * mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -300,71 +338,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -559,6 +535,126 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list, callers should either
+ * make a copy of the list or trim it back to it's original length after
+ * calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *)lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *)rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *)get_rightop(rinfo->clause);
+			else
+				var = (Var *)get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false; /* can't prove rel to be distinct over clause_list */
+}
+
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's nothing to prove
+		 * uniqueness on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 32f903d..20830db 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -140,13 +140,12 @@ static BitmapAnd *make_bitmap_and(List *bitmapplans);
 static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
-			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  Plan *lefttree, Plan *righttree, JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -161,7 +160,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2359,7 +2358,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -2653,7 +2652,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -2779,7 +2778,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3919,7 +3918,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3929,8 +3928,9 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 	node->nestParams = nestParams;
 
 	return node;
@@ -3943,7 +3943,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3954,8 +3954,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
@@ -4003,7 +4004,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -4018,8 +4019,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index f2dce6a..4dac6e8 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1110,6 +1110,7 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	sjinfo->is_unique_join = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 894f968..1e723a9 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -185,6 +185,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Analyze joins to find out which ones have a unique inner side */
+	mark_unique_joins(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index ec0910d..e164026 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1589,6 +1589,7 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  *
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_nestloop
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1604,6 +1605,7 @@ NestPath *
 create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1657,6 +1659,7 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->jointype = jointype;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
+	pathnode->unique_inner = unique_inner;
 	pathnode->joinrestrictinfo = restrict_clauses;
 
 	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
@@ -1671,6 +1674,7 @@ create_nestloop_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+  * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_mergejoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'outer_path' is the outer path
@@ -1687,6 +1691,7 @@ MergePath *
 create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -1715,6 +1720,7 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
@@ -1732,6 +1738,7 @@ create_mergejoin_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_hashjoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1746,6 +1753,7 @@ HashPath *
 create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1784,6 +1792,7 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5ccf470..d0e2b89 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1650,6 +1650,7 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 37086c6..76b7900 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -604,6 +604,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 5393005..66a0952 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1078,6 +1078,7 @@ typedef struct JoinPath
 
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
@@ -1454,6 +1455,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -1685,6 +1687,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * unique_inner is set to True if the planner has determined that the inner
+ *		side of the join can at most produce one tuple for each outer tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -1693,6 +1697,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		unique_inner;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index ac21a3a..bb7f176 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -120,7 +120,7 @@ extern void cost_group(Path *path, PlannerInfo *root,
 		   double input_tuples);
 extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 8fb9eda..46982fb 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -95,6 +95,7 @@ extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_pat
 extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -107,6 +108,7 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -121,6 +123,7 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index f96e9ee..0d91157 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -125,7 +125,11 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+								List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 64f046e..5662bf6 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3302,6 +3302,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Unique Inner: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3309,12 +3310,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Unique Inner: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3352,9 +3354,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3362,9 +3366,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3380,7 +3386,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3413,9 +3419,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3423,12 +3431,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Unique Inner: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3445,7 +3456,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3479,9 +3490,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3489,12 +3502,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Unique Inner: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3514,7 +3530,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3546,14 +3562,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Unique Inner: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Unique Inner: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Unique Inner: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3563,7 +3582,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3593,9 +3612,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Unique Inner: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Unique Inner: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3607,7 +3628,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3632,11 +3653,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Unique Inner: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Unique Inner: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Unique Inner: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3652,7 +3676,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3678,10 +3702,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Unique Inner: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Unique Inner: No
          ->  Nested Loop
                Output: tt1.f1
+               Unique Inner: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3691,6 +3718,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Unique Inner: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3705,7 +3733,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3735,13 +3763,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Unique Inner: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Unique Inner: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Unique Inner: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Unique Inner: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3758,7 +3790,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3815,6 +3847,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Unique Inner: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3827,7 +3860,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -4713,12 +4746,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4745,12 +4779,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4778,6 +4813,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Unique Inner: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4785,7 +4821,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4805,12 +4841,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Unique Inner: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4832,10 +4869,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Unique Inner: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Unique Inner: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4844,7 +4883,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4890,16 +4929,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Unique Inner: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -4966,13 +5007,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Unique Inner: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Unique Inner: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Unique Inner: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Unique Inner: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -4988,7 +5033,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5006,17 +5051,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Unique Inner: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Unique Inner: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Unique Inner: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Unique Inner: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Unique Inner: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5038,7 +5088,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5051,16 +5101,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Unique Inner: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Unique Inner: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5073,10 +5125,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Unique Inner: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Unique Inner: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5085,7 +5139,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5112,10 +5166,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Unique Inner: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5137,7 +5193,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5235,3 +5291,260 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Inner: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Unique Inner: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Inner: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Inner: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(13 rows)
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Inner: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(13 rows)
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+         QUERY PLAN          
+-----------------------------
+ Hash Join
+   Output: j1.id, (1)
+   Unique Inner: No
+   Hash Cond: (j1.id = (1))
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: (1)
+         ->  Result
+               Output: 1
+(10 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Unique Inner: No
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Inner: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Inner: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 00ef421..83f0b7a 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2012,12 +2012,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2038,11 +2039,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index de64ca7..b993eb2 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -783,6 +783,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Unique Inner: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -791,7 +792,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -811,6 +812,7 @@ select * from int4_tbl o where (f1, f1) in
 ----------------------------------------------------------------------
  Hash Join
    Output: o.f1
+   Unique Inner: No
    Hash Cond: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -827,7 +829,7 @@ select * from int4_tbl o where (f1, f1) in
                            Group Key: i.f1
                            ->  Seq Scan on public.int4_tbl i
                                  Output: i.f1
-(18 rows)
+(19 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 8e5463a..8967cf0 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2136,6 +2136,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 3) AND leakproof(t1_5.a))
@@ -2153,6 +2154,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t11
                            Output: t11.ctid, t11.a, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 3) AND leakproof(t11.a))
@@ -2170,6 +2172,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 3) AND leakproof(t12_2.a))
@@ -2187,6 +2190,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 3) AND leakproof(t111_3.a))
@@ -2197,7 +2201,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 3)
-(73 rows)
+(77 rows)
 
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
 SELECT * FROM v1 WHERE a=100; -- Nothing should have been changed to 100
@@ -2226,6 +2230,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 8) AND leakproof(t1_5.a))
@@ -2243,6 +2248,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t11
                            Output: t11.a, t11.ctid, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 8) AND leakproof(t11.a))
@@ -2260,6 +2266,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 8) AND leakproof(t12_2.a))
@@ -2277,6 +2284,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 8) AND leakproof(t111_3.a))
@@ -2287,7 +2295,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 8)
-(73 rows)
+(77 rows)
 
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
 NOTICE:  snooped value: 8
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 137420d..c64d433 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2183,6 +2183,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2190,6 +2191,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2197,6 +2199,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2204,12 +2207,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 0358d00..261ef90 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1678,3 +1678,98 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
unique_joins_17-12-2015_delta.patchapplication/octet-stream; name=unique_joins_17-12-2015_delta.patchDownload
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 4eae85f..ef588d1 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,9 +306,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * When the inner side is unique or we're performing a
-					 * semijoin, we'll consider returning the first match, but
-					 * after that we're done with this outer tuple.
+					 * In the event that the inner side has been detected to be
+					 * unique, as an optimization we can skip searching for any
+					 * subsequent matching inner tuples, as none will exist.
+					 * For semijoins unique_inner will always be true, although
+					 * in this case we don't match another inner tuple as this
+					 * is the required semi join behavior.
 					 */
 					if (node->js.unique_inner)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 2cd532d..0445d66 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,9 +840,12 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * When the inner side is unique or we're performing a
-					 * semijoin, we'll consider returning the first match, but
-					 * after that we're done with this outer tuple.
+					 * In the event that the inner side has been detected to be
+					 * unique, as an optimization we can skip searching for any
+					 * subsequent matching inner tuples, as none will exist.
+					 * For semijoins unique_inner will always be true, although
+					 * in this case we don't match another inner tuple as this
+					 * is the required semi join behavior.
 					 */
 					if (node->js.unique_inner)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 6f0297d..cfb9c9d 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,9 +247,12 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * When the inner side is unique or we're performing a semijoin,
-			 * we'll consider returning the first match, but after that we're
-			 * done with this outer tuple.
+			 * In the event that the inner side has been detected to be unique,
+			 * as an optimization we can skip searching for any subsequent
+			 * matching inner tuples, as none will exist. For semijoins
+			 * unique_inner will always be true, although in this case we don't
+			 * match another inner tuple as this is the required semi join
+			 * behavior.
 			 */
 			if (node->js.unique_inner)
 				node->nl_NeedNewOuter = true;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index d74e496..13e31a4 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -85,7 +85,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	ListCell   *lc;
 	bool		unique_inner;
 
-	/* left joins were already been analyzed for uniqueness in mark_unique_joins() */
+	/* left joins were already analyzed for uniqueness in mark_unique_joins() */
 	if (jointype == JOIN_LEFT)
 		unique_inner = sjinfo->is_unique_join;
 	else if (jointype == JOIN_INNER &&
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index f583477..2262183 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -42,8 +42,8 @@ static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
 /*
  * mark_unique_joins
-		Analyze joins in order to determine if their inner side is unique based
-		on the join condition.
+ *		Analyze joins in order to determine if their inner side is unique based
+ *		on the join condition.
  */
 void
 mark_unique_joins(PlannerInfo *root, List *joinlist)
@@ -617,6 +617,7 @@ rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
 	}
 	return false; /* can't prove rel to be distinct over clause_list */
 }
+
 /*
  * rel_supports_distinctness
  *		Returns true if rel has some properties which can prove the relation
#44Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: David Rowley (#43)
Re: Performance improvement for joins where outer side is unique

Hi,

On 12/16/2015 11:40 PM, David Rowley wrote:

On 17 December 2015 at 05:02, Tomas Vondra <tomas.vondra@2ndquadrant.com
<mailto:tomas.vondra@2ndquadrant.com>> wrote:

0) I know the patch does not tweak costing - any plans in this

direction? Would it be possible to simply use the costing used by
semijoin?

Many thanks for looking at this.

The patch does tweak the costings so that unique joins are costed in the
same way as semi joins. It's a very small change, so easy to miss.

Thanks. I missed that bit somehow.

For example, see the change in initial_cost_nestloop()

-       if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+       if (jointype == JOIN_SEMI ||
+               jointype == JOIN_ANTI ||
+               unique_inner)
{

Also see the changes in final_cost_nestloop() and final_cost_hashjoin()

1) nodeHashjoin.c (and other join nodes)

I've noticed we have this in the ExecHashJoin() method:

/*
* When the inner side is unique or we're performing a
* semijoin, we'll consider returning the first match, but
* after that we're done with this outer tuple.
*/
if (node->js.unique_inner)
node->hj_JoinState = HJ_NEED_NEW_OUTER;

That seems a bit awkward because the comment speaks about unique
joins *OR* semijoins, but the check only references unique_inner.
That of course works because we do this in ExecInitHashJoin():

switch (node->join.jointype)
{
case JOIN_SEMI:
hjstate->js.unique_inner = true;
/* fall through */

Either some semijoins may not be unique - in that case the naming is
misleading at best, and we either need to find a better name or
simply check two conditions like this:

if (node->js.unique_inner || node->join.type == JOIN_SEMI)
node->hj_JoinState = HJ_NEED_NEW_OUTER;

Or all semijoins are unique joins, and in that case the comment may
need rephrasing. But more importantly, it begs the question why
we're detecting this in the executor and not in the planner? Because
if we detect it in executor, we can't use this bit in costing, for
example.

The reason not to detect that in the planner is simply that unique_join
is meant to mean that the relation on the inner side of the join will at
most contain a single Tuple which matches each outer relation tuple,
based on the join condition. I've added no detection for this in semi
joins, and I'd rather not go blindly setting the flag to true in the
planner as it simply may not be true for the semi join. At the moment
that might not matter as we're only using the unique_join flag as an
optimization in the join nodes, but I'd prefer not to do this as its
likely we'll want to do more with this flag later, and I'd rather we
keep the meaning well defined. You might argue that this is also true
for semi joins, but if down the road somewhere we want to perform some
checks on the inner relation before the join takes place, and in that
case the Tuples of the relation might not have the same properties we
claim they do.

But you're right that reusing the flag in the join nodes is not ideal,
and the comment is not that great either. I'd really rather go down the
path of either renaming the variable, or explaining this better in the
comment. It seems unnecessary to check both for each tuple being joined.
I'd imagine that might add a little overhead to joins which are not unique.

I'd be very surprised it that had any measurable impact.

How about changing the comment to:

/*
* In the event that the inner side has been detected to be
* unique, as an optimization we can skip searching for any
* subsequent matching inner tuples, as none will exist.
* For semijoins unique_inner will always be true, although
* in this case we don't match another inner tuple as this
* is the required semi join behavior.
*/

Alternatively or additionally we can rename the variable in the executor
state, although I've not thought of a name yet that I don't find overly
verbose: unique_inner_or_semi_join, match_first_tuple_only.

I'd go with match_first_tuple_only.

2) analyzejoins.c

I see that this code in join_is_removable()

innerrel = find_base_rel(root, innerrelid);

if (innerrel->reloptkind != RELOPT_BASEREL)
return false;

was rewritten like this:

innerrel = find_base_rel(root, innerrelid);

Assert(innerrel->reloptkind == RELOPT_BASEREL);

That suggests that previously the function expected cases where
reloptkind may not be RELOPT_BASEREL, but now it'll error out int
such cases. I haven't noticed any changes in the surrounding code
that'd guarantee this won't happen, but I also haven't been able to
come up with an example triggering the assert (haven't been trying
too hard). How do we know the assert() makes sense?

I'd have changed this as this should be covered by the if
(!sjinfo->is_unique_join || a few lines up. mark_unique_joins() only
sets is_unique_join to true if specialjoin_is_unique_join() returns true
and that function tests reloptkind to ensure its set to RELOPT_BASEREL
and return false if the relation is not. Perhaps what is missing is a
comment to explain the function should only be used on RELOPT_BASEREL
type relations.

Yeah, I think it'd be good to document this contract somewhere. Either
in the comment before the function, or perhaps right above the Assert().

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

#45Simon Riggs
simon@2ndQuadrant.com
In reply to: Tomas Vondra (#44)
Re: Performance improvement for joins where outer side is unique

On 17 December 2015 at 00:17, Tomas Vondra <tomas.vondra@2ndquadrant.com>
wrote:

1) nodeHashjoin.c (and other join nodes)

I've noticed we have this in the ExecHashJoin() method:

/*
* When the inner side is unique or we're performing a
* semijoin, we'll consider returning the first match, but
* after that we're done with this outer tuple.
*/
if (node->js.unique_inner)
node->hj_JoinState = HJ_NEED_NEW_OUTER;

That seems a bit awkward because the comment speaks about unique
joins *OR* semijoins, but the check only references unique_inner.
That of course works because we do this in ExecInitHashJoin():

switch (node->join.jointype)
{
case JOIN_SEMI:
hjstate->js.unique_inner = true;
/* fall through */

Either some semijoins may not be unique - in that case the naming is
misleading at best, and we either need to find a better name or
simply check two conditions like this:

if (node->js.unique_inner || node->join.type == JOIN_SEMI)
node->hj_JoinState = HJ_NEED_NEW_OUTER;

Or all semijoins are unique joins, and in that case the comment may
need rephrasing. But more importantly, it begs the question why
we're detecting this in the executor and not in the planner? Because
if we detect it in executor, we can't use this bit in costing, for
example.

The reason not to detect that in the planner is simply that unique_join
is meant to mean that the relation on the inner side of the join will at
most contain a single Tuple which matches each outer relation tuple,
based on the join condition. I've added no detection for this in semi
joins, and I'd rather not go blindly setting the flag to true in the
planner as it simply may not be true for the semi join. At the moment
that might not matter as we're only using the unique_join flag as an
optimization in the join nodes, but I'd prefer not to do this as its
likely we'll want to do more with this flag later, and I'd rather we
keep the meaning well defined. You might argue that this is also true
for semi joins, but if down the road somewhere we want to perform some
checks on the inner relation before the join takes place, and in that
case the Tuples of the relation might not have the same properties we
claim they do.

But you're right that reusing the flag in the join nodes is not ideal,
and the comment is not that great either. I'd really rather go down the
path of either renaming the variable, or explaining this better in the
comment. It seems unnecessary to check both for each tuple being joined.
I'd imagine that might add a little overhead to joins which are not
unique.

I'd be very surprised it that had any measurable impact.

How about changing the comment to:

/*
* In the event that the inner side has been detected to be
* unique, as an optimization we can skip searching for any
* subsequent matching inner tuples, as none will exist.
* For semijoins unique_inner will always be true, although
* in this case we don't match another inner tuple as this
* is the required semi join behavior.
*/

Alternatively or additionally we can rename the variable in the executor
state, although I've not thought of a name yet that I don't find overly
verbose: unique_inner_or_semi_join, match_first_tuple_only.

I'd go with match_first_tuple_only.

+1

unique_inner is a state that has been detected, match_first_tuple_only is
the action we take as a result.

--
Simon Riggs http://www.2ndQuadrant.com/
<http://www.2ndquadrant.com/&gt;
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#46David Rowley
david.rowley@2ndquadrant.com
In reply to: Simon Riggs (#45)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 17 December 2015 at 19:11, Simon Riggs <simon@2ndquadrant.com> wrote:

On 17 December 2015 at 00:17, Tomas Vondra <tomas.vondra@2ndquadrant.com>
wrote:

I'd go with match_first_tuple_only.

+1

unique_inner is a state that has been detected, match_first_tuple_only is
the action we take as a result.

Ok great. I've made it so in the attached. This means the comment in the
join code where we perform the skip can be a bit less verbose and all the
details can go in where we're actually setting the match_first_tuple_only
to true.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_18-12-2015_843fb71.patchapplication/octet-stream; name=unique_joins_18-12-2015_843fb71.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 12dae77..d1a9729 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1221,6 +1221,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *)plan)->unique_inner ? "Yes" : "No";
+				ExplainPropertyText("Unique Inner", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 1d78cdf..f0886ef 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need 1 inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -453,6 +453,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.match_first_tuple_only = node->join.unique_inner;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -498,8 +506,11 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first tuple only */
+			hjstate->js.match_first_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 34b6cf6..f0a586a 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need 1 inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1487,6 +1487,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.match_first_tuple_only = node->join.unique_inner;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1553,8 +1561,11 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first tuple only */
+			mergestate->js.match_first_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e66bcda..226a40a 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need 1 inner tuple to
+			 * match.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.match_first_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -311,6 +311,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.match_first_tuple_only = node->join.unique_inner;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -354,8 +362,11 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first tuple only */
+			nlstate->js.match_first_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index ba04b72..421f51e 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2058,6 +2058,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(is_unique_join);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 356fcaf..596bea7 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -837,6 +837,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(is_unique_join);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 63fae82..54b01fa 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2050,6 +2050,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(is_unique_join);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 990486c..35cec95 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1834,7 +1834,7 @@ cost_group(Path *path, PlannerInfo *root,
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors)
@@ -1868,7 +1868,9 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI ||
+		unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -1939,7 +1941,9 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI ||
+		path->unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -2809,7 +2813,9 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		path->jpath.unique_inner)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 53d8fdd..13e31a4 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -44,7 +45,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+										   RelOptInfo *innerrel);
 
 /*
  * add_paths_to_joinrel
@@ -81,11 +83,44 @@ add_paths_to_joinrel(PlannerInfo *root,
 	JoinPathExtraData extra;
 	bool		mergejoin_allowed = true;
 	ListCell   *lc;
+	bool		unique_inner;
+
+	/* left joins were already analyzed for uniqueness in mark_unique_joins() */
+	if (jointype == JOIN_LEFT)
+		unique_inner = sjinfo->is_unique_join;
+	else if (jointype == JOIN_INNER &&
+		restrictlist != NIL &&
+		rel_supports_distinctness(root, innerrel))
+	{
+		/*
+		 * remember the number of items that were in the restrictlist as
+		 * the call to relation_has_unique_index_for may add more items
+		 * which we'll need to remove later.
+		 */
+		int org_len = list_length(restrictlist);
+
+		/*
+		 * rel_is_distinct_for requires restrict infos to have the
+		 * correct clause direction info
+		 */
+		foreach(lc, restrictlist)
+		{
+			clause_sides_match_join((RestrictInfo *)lfirst(lc),
+				outerrel, innerrel);
+		}
+		unique_inner = rel_is_distinct_for(root, innerrel, restrictlist);
+
+		/* Remove any list items added by rel_is_distinct_for */
+		list_truncate(restrictlist, org_len);
+	}
+	else
+		unique_inner = false;	/* we can't prove uniqueness */
 
 	extra.restrictlist = restrictlist;
 	extra.mergeclause_list = NIL;
 	extra.sjinfo = sjinfo;
 	extra.param_source_rels = NULL;
+	extra.unique_inner = unique_inner;
 
 	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
@@ -301,7 +336,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * The latter two steps are expensive enough to make this two-phase
 	 * methodology worthwhile.
 	 */
-	initial_cost_nestloop(root, &workspace, jointype,
+	initial_cost_nestloop(root, &workspace, jointype, extra->unique_inner,
 						  outer_path, inner_path,
 						  extra->sjinfo, &extra->semifactors);
 
@@ -313,6 +348,7 @@ try_nestloop_path(PlannerInfo *root,
 				 create_nestloop_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
@@ -390,6 +426,7 @@ try_mergejoin_path(PlannerInfo *root,
 				 create_mergejoin_path(root,
 									   joinrel,
 									   jointype,
+									   extra->unique_inner,
 									   &workspace,
 									   extra->sjinfo,
 									   outer_path,
@@ -455,6 +492,7 @@ try_hashjoin_path(PlannerInfo *root,
 				 create_hashjoin_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index ad58058..d6a9161 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -701,6 +701,7 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 		/* we don't bother trying to make the remaining fields valid */
 		sjinfo->lhs_strict = false;
 		sjinfo->delay_upper_joins = false;
+		sjinfo->is_unique_join = false;
 		sjinfo->semi_can_btree = false;
 		sjinfo->semi_can_hash = false;
 		sjinfo->semi_operators = NIL;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index d188d97..803350b 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -33,11 +33,37 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+/*
+ * mark_unique_joins
+ *		Analyze joins in order to determine if their inner side is unique based
+ *		on the join condition.
+ */
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs that have not already
+		 * been marked as unique by a previous call.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT &&
+			!sjinfo->is_unique_join &&
+			specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -91,6 +117,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to mark some joins as unique which we could
+		 * not do before
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -151,17 +183,17 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must not duplicate its outer side and must be a non-delaying left
+	 * join to a single baserel, else we aren't going to be able to do anything
+	 * with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->is_unique_join ||
+		sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -170,38 +202,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	/* Must be true as is_unique_join can only be set to true for base rels */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -212,7 +214,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -253,6 +256,44 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most 1 inner row for any single outer row. False is returned if there's
+ *		insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -274,10 +315,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we can't
+			 * mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -300,71 +339,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -559,6 +536,126 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list, callers should either
+ * make a copy of the list or trim it back to it's original length after
+ * calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *)lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *)rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *)get_rightop(rinfo->clause);
+			else
+				var = (Var *)get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false; /* can't prove rel to be distinct over clause_list */
+}
+
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's nothing to prove
+		 * uniqueness on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 32f903d..20830db 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -140,13 +140,12 @@ static BitmapAnd *make_bitmap_and(List *bitmapplans);
 static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
-			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  Plan *lefttree, Plan *righttree, JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -161,7 +160,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2359,7 +2358,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -2653,7 +2652,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -2779,7 +2778,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3919,7 +3918,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3929,8 +3928,9 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 	node->nestParams = nestParams;
 
 	return node;
@@ -3943,7 +3943,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3954,8 +3954,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
@@ -4003,7 +4004,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -4018,8 +4019,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index f2dce6a..4dac6e8 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1110,6 +1110,7 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	sjinfo->is_unique_join = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 894f968..1e723a9 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -185,6 +185,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Analyze joins to find out which ones have a unique inner side */
+	mark_unique_joins(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index ec0910d..e164026 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1589,6 +1589,7 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  *
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_nestloop
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1604,6 +1605,7 @@ NestPath *
 create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1657,6 +1659,7 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->jointype = jointype;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
+	pathnode->unique_inner = unique_inner;
 	pathnode->joinrestrictinfo = restrict_clauses;
 
 	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
@@ -1671,6 +1674,7 @@ create_nestloop_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+  * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_mergejoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'outer_path' is the outer path
@@ -1687,6 +1691,7 @@ MergePath *
 create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -1715,6 +1720,7 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
@@ -1732,6 +1738,7 @@ create_mergejoin_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_hashjoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1746,6 +1753,7 @@ HashPath *
 create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1784,6 +1792,7 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5ccf470..803da46 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1650,6 +1650,9 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		match_first_tuple_only;	/* True if we should move to the next
+										 * outer tuple after matching first
+										 * inner tuple */
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 37086c6..76b7900 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -604,6 +604,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 5393005..66a0952 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1078,6 +1078,7 @@ typedef struct JoinPath
 
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
@@ -1454,6 +1455,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -1685,6 +1687,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * unique_inner is set to True if the planner has determined that the inner
+ *		side of the join can at most produce one tuple for each outer tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -1693,6 +1697,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		unique_inner;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index ac21a3a..bb7f176 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -120,7 +120,7 @@ extern void cost_group(Path *path, PlannerInfo *root,
 		   double input_tuples);
 extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 8fb9eda..46982fb 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -95,6 +95,7 @@ extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_pat
 extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -107,6 +108,7 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -121,6 +123,7 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index f96e9ee..0d91157 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -125,7 +125,11 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+								List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 64f046e..5662bf6 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3302,6 +3302,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Unique Inner: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3309,12 +3310,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Unique Inner: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3352,9 +3354,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3362,9 +3366,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3380,7 +3386,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3413,9 +3419,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3423,12 +3431,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Unique Inner: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3445,7 +3456,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3479,9 +3490,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3489,12 +3502,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Unique Inner: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3514,7 +3530,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3546,14 +3562,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Unique Inner: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Unique Inner: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Unique Inner: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3563,7 +3582,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3593,9 +3612,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Unique Inner: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Unique Inner: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3607,7 +3628,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3632,11 +3653,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Unique Inner: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Unique Inner: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Unique Inner: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3652,7 +3676,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3678,10 +3702,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Unique Inner: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Unique Inner: No
          ->  Nested Loop
                Output: tt1.f1
+               Unique Inner: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3691,6 +3718,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Unique Inner: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3705,7 +3733,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3735,13 +3763,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Unique Inner: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Unique Inner: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Unique Inner: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Unique Inner: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3758,7 +3790,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3815,6 +3847,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Unique Inner: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3827,7 +3860,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -4713,12 +4746,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4745,12 +4779,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4778,6 +4813,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Unique Inner: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4785,7 +4821,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4805,12 +4841,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Unique Inner: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4832,10 +4869,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Unique Inner: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Unique Inner: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4844,7 +4883,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4890,16 +4929,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Unique Inner: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -4966,13 +5007,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Unique Inner: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Unique Inner: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Unique Inner: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Unique Inner: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -4988,7 +5033,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5006,17 +5051,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Unique Inner: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Unique Inner: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Unique Inner: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Unique Inner: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Unique Inner: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5038,7 +5088,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5051,16 +5101,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Unique Inner: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Unique Inner: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5073,10 +5125,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Unique Inner: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Unique Inner: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5085,7 +5139,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5112,10 +5166,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Unique Inner: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5137,7 +5193,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5235,3 +5291,260 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Inner: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Unique Inner: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Inner: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Inner: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(13 rows)
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Inner: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(13 rows)
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+         QUERY PLAN          
+-----------------------------
+ Hash Join
+   Output: j1.id, (1)
+   Unique Inner: No
+   Hash Cond: (j1.id = (1))
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: (1)
+         ->  Result
+               Output: 1
+(10 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Unique Inner: No
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Inner: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Inner: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 00ef421..83f0b7a 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2012,12 +2012,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2038,11 +2039,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index de64ca7..b993eb2 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -783,6 +783,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Unique Inner: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -791,7 +792,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -811,6 +812,7 @@ select * from int4_tbl o where (f1, f1) in
 ----------------------------------------------------------------------
  Hash Join
    Output: o.f1
+   Unique Inner: No
    Hash Cond: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -827,7 +829,7 @@ select * from int4_tbl o where (f1, f1) in
                            Group Key: i.f1
                            ->  Seq Scan on public.int4_tbl i
                                  Output: i.f1
-(18 rows)
+(19 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 8e5463a..8967cf0 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2136,6 +2136,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 3) AND leakproof(t1_5.a))
@@ -2153,6 +2154,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t11
                            Output: t11.ctid, t11.a, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 3) AND leakproof(t11.a))
@@ -2170,6 +2172,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 3) AND leakproof(t12_2.a))
@@ -2187,6 +2190,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 3) AND leakproof(t111_3.a))
@@ -2197,7 +2201,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 3)
-(73 rows)
+(77 rows)
 
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
 SELECT * FROM v1 WHERE a=100; -- Nothing should have been changed to 100
@@ -2226,6 +2230,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 8) AND leakproof(t1_5.a))
@@ -2243,6 +2248,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t11
                            Output: t11.a, t11.ctid, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 8) AND leakproof(t11.a))
@@ -2260,6 +2266,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 8) AND leakproof(t12_2.a))
@@ -2277,6 +2284,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 8) AND leakproof(t111_3.a))
@@ -2287,7 +2295,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 8)
-(73 rows)
+(77 rows)
 
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
 NOTICE:  snooped value: 8
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 137420d..c64d433 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2183,6 +2183,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2190,6 +2191,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2197,6 +2199,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2204,12 +2207,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 0358d00..261ef90 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1678,3 +1678,98 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- a subquery with an empty FROM clause should be marked as unique.
+explain (verbose, costs off)
+select * from j1
+inner join (select 1 id offset 0) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#47Michael Paquier
michael.paquier@gmail.com
In reply to: David Rowley (#46)
Re: Performance improvement for joins where outer side is unique

On Thu, Dec 17, 2015 at 10:17 PM, David Rowley
<david.rowley@2ndquadrant.com> wrote:

On 17 December 2015 at 19:11, Simon Riggs <simon@2ndquadrant.com> wrote:

On 17 December 2015 at 00:17, Tomas Vondra <tomas.vondra@2ndquadrant.com>
wrote:

I'd go with match_first_tuple_only.

+1

unique_inner is a state that has been detected, match_first_tuple_only is
the action we take as a result.

Ok great. I've made it so in the attached. This means the comment in the
join code where we perform the skip can be a bit less verbose and all the
details can go in where we're actually setting the match_first_tuple_only to
true.

Patch moved to next CF because of a lack of reviews on the new patch,
and because the last patch has been posted not so long ago.
--
Michael

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

#48Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: David Rowley (#46)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

Hi,

On 12/17/2015 02:17 PM, David Rowley wrote:

On 17 December 2015 at 19:11, Simon Riggs <simon@2ndquadrant.com
<mailto:simon@2ndquadrant.com>> wrote:

On 17 December 2015 at 00:17, Tomas Vondra
<tomas.vondra@2ndquadrant.com <mailto:tomas.vondra@2ndquadrant.com>>
wrote:

I'd go with match_first_tuple_only.

+1

unique_inner is a state that has been detected,
match_first_tuple_only is the action we take as a result.

Ok great. I've made it so in the attached. This means the comment in the
join code where we perform the skip can be a bit less verbose and all
the details can go in where we're actually setting the
match_first_tuple_only to true.

OK. I've looked at the patch again today, and it seems broken bv
45be99f8 as the partial paths were not passing the unique_inner to the
create_*_path() functions. The attached patch should fix that.

Otherwise I think the patch is ready for committer - I think this is a
valuable optimization and I see nothing wrong with the code.

Any objections to marking it accordingly?

regards

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

Attachments:

unijoin-partial.patchapplication/x-patch; name=unijoin-partial.patchDownload
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index be410d2..1ec2ddc 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -411,7 +411,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * Before creating a path, get a quick lower bound on what it is likely
 	 * to cost.  Bail out right away if it looks terrible.
 	 */
-	initial_cost_nestloop(root, &workspace, jointype,
+	initial_cost_nestloop(root, &workspace, jointype, extra->unique_inner,
 						  outer_path, inner_path,
 						  extra->sjinfo, &extra->semifactors);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
@@ -422,6 +422,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 			 create_nestloop_path(root,
 								  joinrel,
 								  jointype,
+								  extra->unique_inner,
 								  &workspace,
 								  extra->sjinfo,
 								  &extra->semifactors,
@@ -622,6 +623,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 			 create_hashjoin_path(root,
 								  joinrel,
 								  jointype,
+								  extra->unique_inner,
 								  &workspace,
 								  extra->sjinfo,
 								  &extra->semifactors,
#49David Rowley
david.rowley@2ndquadrant.com
In reply to: Tomas Vondra (#48)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 23 January 2016 at 05:36, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

OK. I've looked at the patch again today, and it seems broken bv 45be99f8 as
the partial paths were not passing the unique_inner to the create_*_path()
functions. The attached patch should fix that.

Thanks for looking at this again, and thanks for fixing that problem.

I've attached an updated patch which incorporates your change, and
also another 2 small tweaks that I made after making another pass over
this myself.

Otherwise I think the patch is ready for committer - I think this is a
valuable optimization and I see nothing wrong with the code.

Any objections to marking it accordingly?

None from me. I think it's had plenty of review time over the last year.

The only other thing I can possibly think of is to apply most of the
analyzejoins.c changes as a refactor patch first. If committer wants
that, I can separate these out, but I'll hold off for a response
before doing that.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_ba5b9cb_2016-01-23.patchapplication/octet-stream; name=unique_joins_ba5b9cb_2016-01-23.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 25d8ca0..11eee03 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1233,6 +1233,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *val = ((Join *)plan)->unique_inner ? "Yes" : "No";
+				ExplainPropertyText("Unique Inner", val, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 369e666..27b732b 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need 1 inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -453,6 +453,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.match_first_tuple_only = node->join.unique_inner;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -498,8 +506,11 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first tuple only */
+			hjstate->js.match_first_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 6db09b8..341cd4a 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need 1 inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1487,6 +1487,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.match_first_tuple_only = node->join.unique_inner;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1553,8 +1561,11 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first tuple only */
+			mergestate->js.match_first_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 555fa09..0313171 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need 1 inner tuple to
+			 * match.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.match_first_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -311,6 +311,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.match_first_tuple_only = node->join.unique_inner;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -354,8 +362,11 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first tuple only */
+			nlstate->js.match_first_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 5877037..44fe74e 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2060,6 +2060,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(is_unique_join);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 08ccc0d..1b204ed 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -837,6 +837,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(is_unique_join);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index b5e0b55..4a9606c 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2055,6 +2055,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(is_unique_join);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 5fc80e7..63ee251 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1864,7 +1864,7 @@ cost_group(Path *path, PlannerInfo *root,
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors)
@@ -1898,7 +1898,9 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI ||
+		unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -1969,7 +1971,9 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI ||
+		path->unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -2839,7 +2843,9 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		path->jpath.unique_inner)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index e61fa58..1ec2ddc 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -50,7 +51,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+										   RelOptInfo *innerrel);
 
 /*
  * add_paths_to_joinrel
@@ -87,11 +89,44 @@ add_paths_to_joinrel(PlannerInfo *root,
 	JoinPathExtraData extra;
 	bool		mergejoin_allowed = true;
 	ListCell   *lc;
+	bool		unique_inner;
+
+	/* left joins were already analyzed for uniqueness in mark_unique_joins() */
+	if (jointype == JOIN_LEFT)
+		unique_inner = sjinfo->is_unique_join;
+	else if (jointype == JOIN_INNER &&
+		restrictlist != NIL &&
+		rel_supports_distinctness(root, innerrel))
+	{
+		/*
+		 * remember the number of items that were in the restrictlist as
+		 * the call to relation_has_unique_index_for may add more items
+		 * which we'll need to remove later.
+		 */
+		int org_len = list_length(restrictlist);
+
+		/*
+		 * rel_is_distinct_for requires restrict infos to have the
+		 * correct clause direction info
+		 */
+		foreach(lc, restrictlist)
+		{
+			clause_sides_match_join((RestrictInfo *)lfirst(lc),
+				outerrel, innerrel);
+		}
+		unique_inner = rel_is_distinct_for(root, innerrel, restrictlist);
+
+		/* Remove any list items added by rel_is_distinct_for */
+		list_truncate(restrictlist, org_len);
+	}
+	else
+		unique_inner = false;	/* we can't prove uniqueness */
 
 	extra.restrictlist = restrictlist;
 	extra.mergeclause_list = NIL;
 	extra.sjinfo = sjinfo;
 	extra.param_source_rels = NULL;
+	extra.unique_inner = unique_inner;
 
 	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
@@ -312,7 +347,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * The latter two steps are expensive enough to make this two-phase
 	 * methodology worthwhile.
 	 */
-	initial_cost_nestloop(root, &workspace, jointype,
+	initial_cost_nestloop(root, &workspace, jointype, extra->unique_inner,
 						  outer_path, inner_path,
 						  extra->sjinfo, &extra->semifactors);
 
@@ -324,6 +359,7 @@ try_nestloop_path(PlannerInfo *root,
 				 create_nestloop_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
@@ -375,7 +411,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * Before creating a path, get a quick lower bound on what it is likely
 	 * to cost.  Bail out right away if it looks terrible.
 	 */
-	initial_cost_nestloop(root, &workspace, jointype,
+	initial_cost_nestloop(root, &workspace, jointype, extra->unique_inner,
 						  outer_path, inner_path,
 						  extra->sjinfo, &extra->semifactors);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
@@ -386,6 +422,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 			 create_nestloop_path(root,
 								  joinrel,
 								  jointype,
+								  extra->unique_inner,
 								  &workspace,
 								  extra->sjinfo,
 								  &extra->semifactors,
@@ -457,6 +494,7 @@ try_mergejoin_path(PlannerInfo *root,
 				 create_mergejoin_path(root,
 									   joinrel,
 									   jointype,
+									   extra->unique_inner,
 									   &workspace,
 									   extra->sjinfo,
 									   outer_path,
@@ -522,6 +560,7 @@ try_hashjoin_path(PlannerInfo *root,
 				 create_hashjoin_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
@@ -584,6 +623,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 			 create_hashjoin_path(root,
 								  joinrel,
 								  jointype,
+								  extra->unique_inner,
 								  &workspace,
 								  extra->sjinfo,
 								  &extra->semifactors,
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 01d4fea..72be870 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -701,6 +701,7 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 		/* we don't bother trying to make the remaining fields valid */
 		sjinfo->lhs_strict = false;
 		sjinfo->delay_upper_joins = false;
+		sjinfo->is_unique_join = false;
 		sjinfo->semi_can_btree = false;
 		sjinfo->semi_can_hash = false;
 		sjinfo->semi_operators = NIL;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index d682db4..74d7fb0 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -33,11 +33,37 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+/*
+ * mark_unique_joins
+ *		Analyze joins in order to determine if their inner side is unique based
+ *		on the join condition.
+ */
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs that have not already
+		 * been marked as unique by a previous call.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT &&
+			!sjinfo->is_unique_join &&
+			specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -91,6 +117,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to mark some joins as unique which we could
+		 * not do before
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -151,17 +183,17 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must not duplicate its outer side and must be a non-delaying left
+	 * join to a single baserel, else we aren't going to be able to do anything
+	 * with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->is_unique_join ||
+		sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -170,38 +202,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	/* Must be true as is_unique_join can only be set to true for base rels */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -212,7 +214,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -253,6 +256,44 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most 1 inner row for any single outer row. False is returned if there's
+ *		insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -274,10 +315,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we can't
+			 * mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -300,71 +339,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -559,6 +536,126 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list, callers should either
+ * make a copy of the list or trim it back to it's original length after
+ * calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *)lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *)rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *)get_rightop(rinfo->clause);
+			else
+				var = (Var *)get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false; /* can't prove rel to be distinct over clause_list */
+}
+
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's nothing to prove
+		 * uniqueness on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index fda4df6..8773a58 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -140,13 +140,12 @@ static BitmapAnd *make_bitmap_and(List *bitmapplans);
 static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
-			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  Plan *lefttree, Plan *righttree, JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -161,7 +160,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(PlannerInfo *root, Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst,
@@ -2364,7 +2363,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -2658,7 +2657,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -2784,7 +2783,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -3924,7 +3923,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -3934,8 +3933,9 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 	node->nestParams = nestParams;
 
 	return node;
@@ -3948,7 +3948,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -3959,8 +3959,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
@@ -4008,7 +4009,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -4023,8 +4024,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 4a906a8..ace5315 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1127,6 +1127,7 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	sjinfo->is_unique_join = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index f4319c6..bc68e95 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -186,6 +186,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Analyze joins to find out which ones have a unique inner side */
+	mark_unique_joins(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 1097a18..1f2faf1 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1883,6 +1883,7 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  *
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_nestloop
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1898,6 +1899,7 @@ NestPath *
 create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1955,6 +1957,7 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->jointype = jointype;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
+	pathnode->unique_inner = unique_inner;
 	pathnode->joinrestrictinfo = restrict_clauses;
 
 	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
@@ -1969,6 +1972,7 @@ create_nestloop_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+  * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_mergejoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'outer_path' is the outer path
@@ -1985,6 +1989,7 @@ MergePath *
 create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -2016,6 +2021,7 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
@@ -2033,6 +2039,7 @@ create_mergejoin_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_hashjoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -2047,6 +2054,7 @@ HashPath *
 create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -2089,6 +2097,7 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 07cd20a..c86373b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1650,6 +1650,9 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		match_first_tuple_only;	/* True if we should move to the next
+										 * outer tuple after matching first
+										 * inner tuple */
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index e823c83..d3c0894 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -604,6 +604,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index b233b62..c443516 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1082,6 +1082,7 @@ typedef struct JoinPath
 
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
@@ -1458,6 +1459,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -1689,6 +1691,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * unique_inner is set to True if the planner has determined that the inner
+ *		side of the join can at most produce one tuple for each outer tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -1697,6 +1701,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		unique_inner;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 78c7cae..78e7539 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -120,7 +120,7 @@ extern void cost_group(Path *path, PlannerInfo *root,
 		   double input_tuples);
 extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index f479981..bd1b082 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -97,6 +97,7 @@ extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_pat
 extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -109,6 +110,7 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -123,6 +125,7 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 7ae7367..ea2826f 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -124,7 +124,11 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+								List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 64f046e..18cfd8e 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3302,6 +3302,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Unique Inner: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3309,12 +3310,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Unique Inner: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3352,9 +3354,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3362,9 +3366,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3380,7 +3386,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3413,9 +3419,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3423,12 +3431,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Unique Inner: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3445,7 +3456,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3479,9 +3490,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3489,12 +3502,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Unique Inner: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3514,7 +3530,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3546,14 +3562,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Unique Inner: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Unique Inner: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Unique Inner: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3563,7 +3582,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3593,9 +3612,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Unique Inner: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Unique Inner: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3607,7 +3628,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3632,11 +3653,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Unique Inner: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Unique Inner: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Unique Inner: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3652,7 +3676,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3678,10 +3702,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Unique Inner: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Unique Inner: No
          ->  Nested Loop
                Output: tt1.f1
+               Unique Inner: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3691,6 +3718,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Unique Inner: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3705,7 +3733,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3735,13 +3763,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Unique Inner: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Unique Inner: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Unique Inner: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Unique Inner: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3758,7 +3790,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3815,6 +3847,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Unique Inner: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3827,7 +3860,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -4713,12 +4746,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4745,12 +4779,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4778,6 +4813,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Unique Inner: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4785,7 +4821,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4805,12 +4841,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Unique Inner: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4832,10 +4869,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Unique Inner: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Unique Inner: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4844,7 +4883,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4890,16 +4929,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Unique Inner: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -4966,13 +5007,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Unique Inner: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Unique Inner: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Unique Inner: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Unique Inner: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -4988,7 +5033,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5006,17 +5051,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Unique Inner: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Unique Inner: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Unique Inner: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Unique Inner: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Unique Inner: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5038,7 +5088,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5051,16 +5101,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Unique Inner: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Unique Inner: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5073,10 +5125,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Unique Inner: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Unique Inner: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5085,7 +5139,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5112,10 +5166,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Unique Inner: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5137,7 +5193,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5235,3 +5291,242 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Inner: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Unique Inner: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- clauseless cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Inner: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Inner: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(13 rows)
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Inner: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  HashAggregate
+               Output: j3.id
+               Group Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+(13 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Unique Inner: No
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Inner: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Inner: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 00ef421..83f0b7a 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2012,12 +2012,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2038,11 +2039,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index de64ca7..b993eb2 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -783,6 +783,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Unique Inner: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -791,7 +792,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -811,6 +812,7 @@ select * from int4_tbl o where (f1, f1) in
 ----------------------------------------------------------------------
  Hash Join
    Output: o.f1
+   Unique Inner: No
    Hash Cond: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -827,7 +829,7 @@ select * from int4_tbl o where (f1, f1) in
                            Group Key: i.f1
                            ->  Seq Scan on public.int4_tbl i
                                  Output: i.f1
-(18 rows)
+(19 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 6c71371..1614e80 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2136,6 +2136,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 3) AND leakproof(t1_5.a))
@@ -2153,6 +2154,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t11
                            Output: t11.ctid, t11.a, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 3) AND leakproof(t11.a))
@@ -2170,6 +2172,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 3) AND leakproof(t12_2.a))
@@ -2187,6 +2190,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 3) AND leakproof(t111_3.a))
@@ -2197,7 +2201,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 3)
-(73 rows)
+(77 rows)
 
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
 SELECT * FROM v1 WHERE a=100; -- Nothing should have been changed to 100
@@ -2226,6 +2230,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 8) AND leakproof(t1_5.a))
@@ -2243,6 +2248,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t11
                            Output: t11.a, t11.ctid, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 8) AND leakproof(t11.a))
@@ -2260,6 +2266,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 8) AND leakproof(t12_2.a))
@@ -2277,6 +2284,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 8) AND leakproof(t111_3.a))
@@ -2287,7 +2295,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 8)
-(73 rows)
+(77 rows)
 
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
 NOTICE:  snooped value: 8
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 137420d..c64d433 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2183,6 +2183,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2190,6 +2191,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2197,6 +2199,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2204,12 +2207,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 0358d00..d69c3f7 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1678,3 +1678,93 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- clauseless cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#50David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#49)
Re: Performance improvement for joins where outer side is unique

On 23/01/2016 12:42 am, "David Rowley" <david.rowley@2ndquadrant.com> wrote:

On 23 January 2016 at 05:36, Tomas Vondra <tomas.vondra@2ndquadrant.com>

wrote:

Otherwise I think the patch is ready for committer - I think this is a
valuable optimization and I see nothing wrong with the code.

Any objections to marking it accordingly?

I've just set this patch back to ready for committer. It was previous
marked as so but lost that status when it was moved to the march fest.

#51Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: David Rowley (#49)
Re: Performance improvement for joins where outer side is unique

I wonder why do we have two identical copies of clause_sides_match_join ...

--
�lvaro Herrera 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

#52David Rowley
david.rowley@2ndquadrant.com
In reply to: Alvaro Herrera (#51)
Re: Performance improvement for joins where outer side is unique

On 5 March 2016 at 10:43, Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

I wonder why do we have two identical copies of clause_sides_match_join ...

Yeah, I noticed the same a while back, and posted about it. Here was
the response:
/messages/by-id/26820.1405522310@sss.pgh.pa.us

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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

#53David Rowley
david.rowley@2ndquadrant.com
In reply to: Tomas Vondra (#48)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 23 January 2016 at 05:36, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

Hi,

On 12/17/2015 02:17 PM, David Rowley wrote:

On 17 December 2015 at 19:11, Simon Riggs <simon@2ndquadrant.com
<mailto:simon@2ndquadrant.com>> wrote:

On 17 December 2015 at 00:17, Tomas Vondra
<tomas.vondra@2ndquadrant.com <mailto:tomas.vondra@2ndquadrant.com>>
wrote:

I'd go with match_first_tuple_only.

+1

unique_inner is a state that has been detected,
match_first_tuple_only is the action we take as a result.

Ok great. I've made it so in the attached. This means the comment in the
join code where we perform the skip can be a bit less verbose and all
the details can go in where we're actually setting the
match_first_tuple_only to true.

OK. I've looked at the patch again today, and it seems broken bv 45be99f8 as
the partial paths were not passing the unique_inner to the create_*_path()
functions. The attached patch should fix that.

Otherwise I think the patch is ready for committer - I think this is a
valuable optimization and I see nothing wrong with the code.

I've attached an updated patch which updates it to fix the conflicts
with the recent upper planner changes.
I also notice that some regression tests, which I think some of which
Tom updated in the upper planner changes have now changed back again
due to the slightly reduced costs on hash and nested loop joins where
the inner side is unique. I checked the costs of one of these by
disabling hash join and noticed that the final totla cost is the same,
so it's not too surprising that they keep switching plans with these
planner changes going in. I verified that these remain as is when I
comment out the cost changing code in this patch.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_dbcecda_2016-03-09.patchapplication/octet-stream; name=unique_joins_dbcecda_2016-03-09.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index ee13136..9af036d 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1245,6 +1245,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *val = ((Join *)plan)->unique_inner ? "Yes" : "No";
+				ExplainPropertyText("Unique Inner", val, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 369e666..27b732b 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need 1 inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -453,6 +453,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.match_first_tuple_only = node->join.unique_inner;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -498,8 +506,11 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first tuple only */
+			hjstate->js.match_first_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 6db09b8..341cd4a 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need 1 inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1487,6 +1487,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.match_first_tuple_only = node->join.unique_inner;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1553,8 +1561,11 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first tuple only */
+			mergestate->js.match_first_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 555fa09..0313171 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need 1 inner tuple to
+			 * match.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.match_first_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -311,6 +311,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.match_first_tuple_only = node->join.unique_inner;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -354,8 +362,11 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first tuple only */
+			nlstate->js.match_first_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index df7c2fa..1f4b77e 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2063,6 +2063,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(is_unique_join);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index b9c3959..f374c36 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -838,6 +838,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(is_unique_join);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 3119b9e..c1bda16 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2265,6 +2265,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(is_unique_join);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 5350329..4f60c3e 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1894,7 +1894,7 @@ cost_group(Path *path, PlannerInfo *root,
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors)
@@ -1928,7 +1928,9 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI ||
+		unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -1999,7 +2001,9 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI ||
+		path->unique_inner)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -2878,7 +2882,9 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		path->jpath.unique_inner)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 3b898da..8a71337 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -50,7 +51,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+										   RelOptInfo *innerrel);
 
 /*
  * add_paths_to_joinrel
@@ -87,11 +89,44 @@ add_paths_to_joinrel(PlannerInfo *root,
 	JoinPathExtraData extra;
 	bool		mergejoin_allowed = true;
 	ListCell   *lc;
+	bool		unique_inner;
+
+	/* left joins were already analyzed for uniqueness in mark_unique_joins() */
+	if (jointype == JOIN_LEFT)
+		unique_inner = sjinfo->is_unique_join;
+	else if (jointype == JOIN_INNER &&
+		restrictlist != NIL &&
+		rel_supports_distinctness(root, innerrel))
+	{
+		/*
+		 * remember the number of items that were in the restrictlist as
+		 * the call to relation_has_unique_index_for may add more items
+		 * which we'll need to remove later.
+		 */
+		int org_len = list_length(restrictlist);
+
+		/*
+		 * rel_is_distinct_for requires restrict infos to have the
+		 * correct clause direction info
+		 */
+		foreach(lc, restrictlist)
+		{
+			clause_sides_match_join((RestrictInfo *)lfirst(lc),
+				outerrel, innerrel);
+		}
+		unique_inner = rel_is_distinct_for(root, innerrel, restrictlist);
+
+		/* Remove any list items added by rel_is_distinct_for */
+		list_truncate(restrictlist, org_len);
+	}
+	else
+		unique_inner = false;	/* we can't prove uniqueness */
 
 	extra.restrictlist = restrictlist;
 	extra.mergeclause_list = NIL;
 	extra.sjinfo = sjinfo;
 	extra.param_source_rels = NULL;
+	extra.unique_inner = unique_inner;
 
 	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
@@ -312,7 +347,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * The latter two steps are expensive enough to make this two-phase
 	 * methodology worthwhile.
 	 */
-	initial_cost_nestloop(root, &workspace, jointype,
+	initial_cost_nestloop(root, &workspace, jointype, extra->unique_inner,
 						  outer_path, inner_path,
 						  extra->sjinfo, &extra->semifactors);
 
@@ -324,6 +359,7 @@ try_nestloop_path(PlannerInfo *root,
 				 create_nestloop_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
@@ -375,7 +411,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * Before creating a path, get a quick lower bound on what it is likely
 	 * to cost.  Bail out right away if it looks terrible.
 	 */
-	initial_cost_nestloop(root, &workspace, jointype,
+	initial_cost_nestloop(root, &workspace, jointype, extra->unique_inner,
 						  outer_path, inner_path,
 						  extra->sjinfo, &extra->semifactors);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
@@ -386,6 +422,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 			 create_nestloop_path(root,
 								  joinrel,
 								  jointype,
+								  extra->unique_inner,
 								  &workspace,
 								  extra->sjinfo,
 								  &extra->semifactors,
@@ -457,6 +494,7 @@ try_mergejoin_path(PlannerInfo *root,
 				 create_mergejoin_path(root,
 									   joinrel,
 									   jointype,
+									   extra->unique_inner,
 									   &workspace,
 									   extra->sjinfo,
 									   outer_path,
@@ -522,6 +560,7 @@ try_hashjoin_path(PlannerInfo *root,
 				 create_hashjoin_path(root,
 									  joinrel,
 									  jointype,
+									  extra->unique_inner,
 									  &workspace,
 									  extra->sjinfo,
 									  &extra->semifactors,
@@ -584,6 +623,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 			 create_hashjoin_path(root,
 								  joinrel,
 								  jointype,
+								  extra->unique_inner,
 								  &workspace,
 								  extra->sjinfo,
 								  &extra->semifactors,
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 01d4fea..72be870 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -701,6 +701,7 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 		/* we don't bother trying to make the remaining fields valid */
 		sjinfo->lhs_strict = false;
 		sjinfo->delay_upper_joins = false;
+		sjinfo->is_unique_join = false;
 		sjinfo->semi_can_btree = false;
 		sjinfo->semi_can_hash = false;
 		sjinfo->semi_operators = NIL;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index d682db4..74d7fb0 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -33,11 +33,37 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+/*
+ * mark_unique_joins
+ *		Analyze joins in order to determine if their inner side is unique based
+ *		on the join condition.
+ */
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs that have not already
+		 * been marked as unique by a previous call.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT &&
+			!sjinfo->is_unique_join &&
+			specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -91,6 +117,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to mark some joins as unique which we could
+		 * not do before
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -151,17 +183,17 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must not duplicate its outer side and must be a non-delaying left
+	 * join to a single baserel, else we aren't going to be able to do anything
+	 * with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->is_unique_join ||
+		sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -170,38 +202,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	/* Must be true as is_unique_join can only be set to true for base rels */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -212,7 +214,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -253,6 +256,44 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most 1 inner row for any single outer row. False is returned if there's
+ *		insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -274,10 +315,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we can't
+			 * mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -300,71 +339,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -559,6 +536,126 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list, callers should either
+ * make a copy of the list or trim it back to it's original length after
+ * calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *)lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *)rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *)get_rightop(rinfo->clause);
+			else
+				var = (Var *)get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false; /* can't prove rel to be distinct over clause_list */
+}
+
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's nothing to prove
+		 * uniqueness on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 3024ff9..91d8e50 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -204,13 +204,12 @@ static BitmapAnd *make_bitmap_and(List *bitmapplans);
 static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
-			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  Plan *lefttree, Plan *righttree, JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -225,7 +224,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3480,7 +3479,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -3783,7 +3782,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -3923,7 +3922,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5022,7 +5021,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5031,8 +5030,9 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 	node->nestParams = nestParams;
 
 	return node;
@@ -5045,7 +5045,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5055,8 +5055,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
@@ -5097,7 +5098,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5111,8 +5112,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.unique_inner = jpath->unique_inner;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 37fb586..7ea7984 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1128,6 +1128,7 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	sjinfo->is_unique_join = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 443e64e..885746b 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -185,6 +185,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Analyze joins to find out which ones have a unique inner side */
+	mark_unique_joins(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index fe5e830..60d774c 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1911,6 +1911,7 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  *
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_nestloop
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -1926,6 +1927,7 @@ NestPath *
 create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -1984,6 +1986,7 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->jointype = jointype;
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
+	pathnode->unique_inner = unique_inner;
 	pathnode->joinrestrictinfo = restrict_clauses;
 
 	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
@@ -1998,6 +2001,7 @@ create_nestloop_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+  * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_mergejoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'outer_path' is the outer path
@@ -2014,6 +2018,7 @@ MergePath *
 create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -2047,6 +2052,7 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
@@ -2064,6 +2070,7 @@ create_mergejoin_path(PlannerInfo *root,
  *
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
+ * 'unique_inner' is the inner side of the join unique on the join condition
  * 'workspace' is the result from initial_cost_hashjoin
  * 'sjinfo' is extra info about the join for selectivity estimation
  * 'semifactors' contains valid data if jointype is SEMI or ANTI
@@ -2078,6 +2085,7 @@ HashPath *
 create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -2121,6 +2129,7 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.jointype = jointype;
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
+	pathnode->jpath.unique_inner = unique_inner;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 064a050..a31e758 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1662,6 +1662,9 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		match_first_tuple_only;	/* True if we should move to the next
+										 * outer tuple after matching first
+										 * inner tuple */
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 5961f2c..c5d4f0c 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -605,6 +605,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 098a486..98f7ed6 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1158,6 +1158,7 @@ typedef struct JoinPath
 
 	Path	   *outerjoinpath;	/* path for the outer side of the join */
 	Path	   *innerjoinpath;	/* path for the inner side of the join */
+	bool		unique_inner;	/* inner rel is unique on the join condition */
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
@@ -1723,6 +1724,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -1955,6 +1957,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * unique_inner is set to True if the planner has determined that the inner
+ *		side of the join can at most produce one tuple for each outer tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -1963,6 +1967,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		unique_inner;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index fea2bb7..3dc06ed 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -120,7 +120,7 @@ extern void cost_group(Path *path, PlannerInfo *root,
 		   double input_tuples);
 extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
-					  JoinType jointype,
+					  JoinType jointype, bool unique_inner,
 					  Path *outer_path, Path *inner_path,
 					  SpecialJoinInfo *sjinfo,
 					  SemiAntiJoinFactors *semifactors);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index df4be93..0a07cb2 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -99,6 +99,7 @@ extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_pat
 extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
@@ -111,6 +112,7 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
+					  bool unique_inner,
 					  JoinCostWorkspace *workspace,
 					  SpecialJoinInfo *sjinfo,
 					  Path *outer_path,
@@ -125,6 +127,7 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
+					 bool unique_inner,
 					 JoinCostWorkspace *workspace,
 					 SpecialJoinInfo *sjinfo,
 					 SemiAntiJoinFactors *semifactors,
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index cd7338a..e79c037 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -90,7 +90,11 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+								List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 601bdb4..f2dbb4a 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -884,29 +884,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index cafbc5e..4c488bc 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3330,6 +3330,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Unique Inner: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3337,12 +3338,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Unique Inner: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3380,9 +3382,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3390,9 +3394,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3408,7 +3414,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3441,9 +3447,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3451,12 +3459,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Unique Inner: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3473,7 +3484,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3507,9 +3518,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Unique Inner: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Unique Inner: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3517,12 +3530,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Unique Inner: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Unique Inner: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Unique Inner: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3542,7 +3558,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3574,14 +3590,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Unique Inner: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Unique Inner: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Unique Inner: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3591,7 +3610,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3621,9 +3640,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Unique Inner: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Unique Inner: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3635,7 +3656,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3660,11 +3681,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Unique Inner: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Unique Inner: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Unique Inner: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3680,7 +3704,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3706,10 +3730,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Unique Inner: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Unique Inner: No
          ->  Nested Loop
                Output: tt1.f1
+               Unique Inner: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3719,6 +3746,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Unique Inner: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3733,7 +3761,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3763,13 +3791,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Unique Inner: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Unique Inner: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Unique Inner: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Unique Inner: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3786,7 +3818,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3843,6 +3875,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Unique Inner: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3855,7 +3888,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -4738,12 +4771,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4770,12 +4804,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4803,6 +4838,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Unique Inner: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4810,7 +4846,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4830,12 +4866,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Unique Inner: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4857,10 +4894,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Unique Inner: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Unique Inner: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4869,7 +4908,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4915,16 +4954,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Unique Inner: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -4991,13 +5032,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Unique Inner: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Unique Inner: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Unique Inner: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Unique Inner: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5013,7 +5058,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5031,17 +5076,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Unique Inner: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Unique Inner: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Unique Inner: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Unique Inner: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Unique Inner: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5063,7 +5113,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5076,16 +5126,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Unique Inner: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Unique Inner: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5098,10 +5150,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Unique Inner: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Unique Inner: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5110,7 +5164,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5137,10 +5191,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Unique Inner: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5162,7 +5218,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5260,3 +5316,247 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Inner: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Unique Inner: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Unique Inner: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- clauseless cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Unique Inner: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Unique Inner: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Inner: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Unique Inner: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(16 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Unique Inner: No
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Inner: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Unique Inner: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 00ef421..83f0b7a 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2012,12 +2012,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2038,11 +2039,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Unique Inner: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index de64ca7..b993eb2 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -783,6 +783,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Unique Inner: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -791,7 +792,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -811,6 +812,7 @@ select * from int4_tbl o where (f1, f1) in
 ----------------------------------------------------------------------
  Hash Join
    Output: o.f1
+   Unique Inner: No
    Hash Cond: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -827,7 +829,7 @@ select * from int4_tbl o where (f1, f1) in
                            Group Key: i.f1
                            ->  Seq Scan on public.int4_tbl i
                                  Output: i.f1
-(18 rows)
+(19 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index c5dfbb5..40c9c26 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2136,6 +2136,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 3) AND leakproof(t1_5.a))
@@ -2153,6 +2154,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t11
                            Output: t11.ctid, t11.a, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 3) AND leakproof(t11.a))
@@ -2170,6 +2172,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 3) AND leakproof(t12_2.a))
@@ -2187,6 +2190,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 3) AND leakproof(t111_3.a))
@@ -2197,7 +2201,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 3)
-(73 rows)
+(77 rows)
 
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
 SELECT * FROM v1 WHERE a=100; -- Nothing should have been changed to 100
@@ -2226,6 +2230,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 8) AND leakproof(t1_5.a))
@@ -2243,6 +2248,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t11
                            Output: t11.a, t11.ctid, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 8) AND leakproof(t11.a))
@@ -2260,6 +2266,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 8) AND leakproof(t12_2.a))
@@ -2277,6 +2284,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Unique Inner: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 8) AND leakproof(t111_3.a))
@@ -2287,7 +2295,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 8)
-(73 rows)
+(77 rows)
 
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
 NOTICE:  snooped value: 8
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 137420d..c64d433 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2183,6 +2183,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2190,6 +2191,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2197,6 +2199,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2204,12 +2207,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Unique Inner: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 3430f91..bb86c10 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1696,3 +1696,93 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- clauseless cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#54Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#53)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

I also notice that some regression tests, which I think some of which
Tom updated in the upper planner changes have now changed back again
due to the slightly reduced costs on hash and nested loop joins where
the inner side is unique.

?? I don't see anything in this patch that touches the same queries
that changed plans in my previous patch.

I do think that the verbosity this adds to the EXPLAIN output is not
desirable at all, at least not in text mode. Another line for every
darn join is a pretty high price.

regards, tom lane

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

#55David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#54)
Re: Performance improvement for joins where outer side is unique

On 9 March 2016 at 13:19, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I do think that the verbosity this adds to the EXPLAIN output is not
desirable at all, at least not in text mode. Another line for every
darn join is a pretty high price.

For me it seems like a good idea to give some sort of indication in
EXPLAIN about this, although I'm not wedded to what I have currently
as being the best and neatest way to do this.
I was however under the impression that you thought this was
reasonable [1]/messages/by-id/8907.1440383377@sss.pgh.pa.us, so I didn't go seeking out any other way. Did you have
another idea, or are you just proposing to remove it for text EXPLAIN?
Perhaps, if you are then the tests can still exist with a non-text
output.

Also in [1]/messages/by-id/8907.1440383377@sss.pgh.pa.us, I disagree with your proposal for being inconsistent with
what's shown with VERBOSE depending on the EXPLAIN output format. If
we were going to do that then I'd rather just switch on verbose for
non-text, but that might make some people unhappy, so I'd rather we
didn't do that either. So how about I remove it from the TEXT output
and just include it in non-text formats when the VERBOSE flag is set?
then change the tests to use... something other than text... YAML
seems most compact.

[1]: /messages/by-id/8907.1440383377@sss.pgh.pa.us

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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

#56Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#55)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

[1] /messages/by-id/8907.1440383377@sss.pgh.pa.us

Oh, okay, I had looked at the many changes in the regression outputs and
jumped to the conclusion that you were printing the info all the time.
Looking closer I see it's only coming out in VERBOSE mode. That's
probably fine. <emilylitella>Never mind</emilylitella>

regards, tom lane

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

#57Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#55)
Re: Performance improvement for joins where outer side is unique

So I started re-reading this thread in preparation for looking at the
patch, and this bit in your initial message jumped out at me:

In all of our join algorithms in the executor, if the join type is SEMI,
we skip to the next outer row once we find a matching inner row. This is
because we don't want to allow duplicate rows in the inner side to
duplicate outer rows in the result set. Obviously this is required per SQL
spec. I believe we can also skip to the next outer row in this case when
we've managed to prove that no other row can possibly exist that matches
the current outer row, due to a unique index or group by/distinct clause
(for subqueries).

I wondered why, instead of inventing an extra semantics-modifying flag,
we couldn't just change the jointype to *be* JOIN_SEMI when we've
discovered that the inner side is unique.

Now of course this only works if the join type was INNER to start with.
If it was a LEFT join, you'd need an "outer semi join" jointype which
we haven't got at the moment. But I wonder whether inventing that
jointype wouldn't let us arrive at a less messy handling of things in
the executor and EXPLAIN. I'm not very enamored of plastering this
"match_first_tuple_only" flag on every join, in part because it doesn't
appear to have sensible semantics for other jointypes such as JOIN_RIGHT.
And I'd really be happier to see the information reflected by join type
than a new line in EXPLAIN, also.

regards, tom lane

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

#58Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#57)
Re: Performance improvement for joins where outer side is unique

I wrote:

I wondered why, instead of inventing an extra semantics-modifying flag,
we couldn't just change the jointype to *be* JOIN_SEMI when we've
discovered that the inner side is unique.

BTW, to clarify: I'm not imagining that we'd make this change in the
query jointree, as for example prepjointree.c might do. That would appear
to add join order constraints, which we don't want. But I'm thinking that
at the instant where we form a join Path, we could change the Path's
jointype to be JOIN_SEMI or JOIN_SEMI_OUTER if we're able to prove the
inner side unique, rather than annotating the Path with a separate flag.
Then that representation is what propagates forward.

It seems like the major intellectual complexity here is to figure out
how to detect inner-side-unique at reasonable cost. I see that for
LEFT joins you're caching that in the SpecialJoinInfos, which is probably
fine. But for INNER joins it looks like you're just doing it over again
for every candidate join, and that seems mighty expensive.

regards, tom lane

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

#59David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#58)
Re: Performance improvement for joins where outer side is unique

On 12 March 2016 at 11:43, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I wrote:

I wondered why, instead of inventing an extra semantics-modifying flag,
we couldn't just change the jointype to *be* JOIN_SEMI when we've
discovered that the inner side is unique.

BTW, to clarify: I'm not imagining that we'd make this change in the
query jointree, as for example prepjointree.c might do. That would appear
to add join order constraints, which we don't want. But I'm thinking that
at the instant where we form a join Path, we could change the Path's
jointype to be JOIN_SEMI or JOIN_SEMI_OUTER if we're able to prove the
inner side unique, rather than annotating the Path with a separate flag.
Then that representation is what propagates forward.

Thanks for looking at this.

Yes that might work, since we'd just be changing the jointype in the
JoinPath, if that path was discarded if favour of, say the commutative
variant, which was not "unique inner", then it shouldn't matter, as
the join type for that path would be the original one.

The thing that might matter is that, this;

explain (costs off) select * from t1 inner join t2 on t1.id=t2.id
QUERY PLAN
------------------------------
Hash Join
Hash Cond: (t1.id = t2.id)
-> Seq Scan on t1
-> Hash
-> Seq Scan on t2

could become;

QUERY PLAN
------------------------------
Hash Semi Join
Hash Cond: (t1.id = t2.id)
-> Seq Scan on t1
-> Hash
-> Seq Scan on t2

Wouldn't that cause quite a bit of confusion? People browsing EXPLAIN
output might be a bit confused at the lack of EXISTS/IN clause in a
query which is showing a Semi Join. Now, we could get around that by
adding JOIN_SEMI_INNER I guess, and just displaying that as a normal
inner join, yet it'll behave exactly like JOIN_SEMI!

And if we do that, then the join node code changes from;

if (node->js.match_first_tuple_only)
node->nl_NeedNewOuter = true;

to;

if (node->js.jointype == JOIN_SEMI || node->js.jointype ==
JOIN_SEMI_INNER || node->js.jointype == JOIN_SEMI_LEFT)
node->nl_NeedNewOuter = true;

which is kinda horrid and adds a few cycles and code without a real
need for it. If we kept the match_first_tuples_only and set it in the
node startup similar to how it is now, or changed JoinType to be some
bitmask mask that had a bit for each piece of a venn diagram, and an
extra for the unique inner... but I'm not so sure I like that idea...

To me it seems neater just to keep the match_first_tuple_only and just
update the planner stuff to use the new jointypes.

Thoughts?

It seems like the major intellectual complexity here is to figure out
how to detect inner-side-unique at reasonable cost. I see that for
LEFT joins you're caching that in the SpecialJoinInfos, which is probably
fine. But for INNER joins it looks like you're just doing it over again
for every candidate join, and that seems mighty expensive.

hmm yeah, perhaps something can be done to cache that, I'm just not
all that sure how much reuse the cache will get used since it
generates the Paths for nestloop/merge/has join methods all at once,
once the unique_inner has been determined for that inner rel, and the
restrict info for whatever the outer rel(s) are. I didn't expect that
to happen twice, so I'm not all that sure if a cache will get
reused...

... thinks more ...

Perhaps maybe if rel x was found to be unique with the join conditions
of rel y, then we can be sure that x is unique against y, z, as that
could only possible add more to the join condition, never less. Is
this what you meant?

... I'll look into that.

The other thing I thought of was to add a dedicated list for unique
indexes in RelOptInfo, this would also allow
rel_supports_distinctness() to do something a bit smarter than just
return false if there's no indexes. That might not buy us much though,
but at least relations tend to have very little unique indexes, even
when they have lots of indexes.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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

#60David G. Johnston
david.g.johnston@gmail.com
In reply to: David Rowley (#59)
Re: Performance improvement for joins where outer side is unique

On Saturday, March 12, 2016, David Rowley <david.rowley@2ndquadrant.com>
wrote:

On 12 March 2016 at 11:43, Tom Lane <tgl@sss.pgh.pa.us <javascript:;>>
wrote:

I wrote:

I wondered why, instead of inventing an extra semantics-modifying flag,
we couldn't just change the jointype to *be* JOIN_SEMI when we've
discovered that the inner side is unique.

BTW, to clarify: I'm not imagining that we'd make this change in the
query jointree, as for example prepjointree.c might do. That would

appear

to add join order constraints, which we don't want. But I'm thinking

that

at the instant where we form a join Path, we could change the Path's
jointype to be JOIN_SEMI or JOIN_SEMI_OUTER if we're able to prove the
inner side unique, rather than annotating the Path with a separate flag.
Then that representation is what propagates forward.

Thanks for looking at this.

Yes that might work, since we'd just be changing the jointype in the
JoinPath, if that path was discarded if favour of, say the commutative
variant, which was not "unique inner", then it shouldn't matter, as
the join type for that path would be the original one.

The thing that might matter is that, this;

explain (costs off) select * from t1 inner join t2 on t1.id=t2.id
QUERY PLAN
------------------------------
Hash Join
Hash Cond: (t1.id = t2.id)
-> Seq Scan on t1
-> Hash
-> Seq Scan on t2

could become;

QUERY PLAN
------------------------------
Hash Semi Join
Hash Cond: (t1.id = t2.id)
-> Seq Scan on t1
-> Hash
-> Seq Scan on t2

Wouldn't that cause quite a bit of confusion? People browsing EXPLAIN
output might be a bit confused at the lack of EXISTS/IN clause in a
query which is showing a Semi Join. Now, we could get around that by
adding JOIN_SEMI_INNER I guess, and just displaying that as a normal
inner join, yet it'll behave exactly like JOIN_SEMI!

Don't the semantics of a SEMI JOIN also state that the output columns only
come from the outer relation? i.e., the inner relation doesn't contribute
either rows or columns to the final result? Or is that simply
an implementation artifact of the fact that the only current way to perform
a semi-join explicitly is via exists/in?

David J.

#61Tom Lane
tgl@sss.pgh.pa.us
In reply to: David G. Johnston (#60)
Re: Performance improvement for joins where outer side is unique

"David G. Johnston" <david.g.johnston@gmail.com> writes:

Don't the semantics of a SEMI JOIN also state that the output columns only
come from the outer relation? i.e., the inner relation doesn't contribute
either rows or columns to the final result? Or is that simply
an implementation artifact of the fact that the only current way to perform
a semi-join explicitly is via exists/in?

I think it's an artifact. What nodes.h actually says about it is you get
the values of one randomly-selected matching inner row, which seems like
a fine definition for the purposes we plan to put it to.

regards, tom lane

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

#62Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#59)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

On 12 March 2016 at 11:43, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I wondered why, instead of inventing an extra semantics-modifying flag,
we couldn't just change the jointype to *be* JOIN_SEMI when we've
discovered that the inner side is unique.

The thing that might matter is that, this;

explain (costs off) select * from t1 inner join t2 on t1.id=t2.id
QUERY PLAN
------------------------------
Hash Join
Hash Cond: (t1.id = t2.id)
-> Seq Scan on t1
-> Hash
-> Seq Scan on t2

could become;

QUERY PLAN
------------------------------
Hash Semi Join
Hash Cond: (t1.id = t2.id)
-> Seq Scan on t1
-> Hash
-> Seq Scan on t2

Wouldn't that cause quite a bit of confusion?

Well, no more than was introduced when we invented semi joins at all.

Now, we could get around that by
adding JOIN_SEMI_INNER I guess, and just displaying that as a normal
inner join, yet it'll behave exactly like JOIN_SEMI!

I'm not that thrilled with having EXPLAIN hide real differences in the
plan from you; if I was, I'd have just lobbied to drop the "unique inner"
annotation from EXPLAIN output altogether.

(I think at one point we'd discussed displaying this in EXPLAIN output
as a different join type, and I'd been against it at the time. What
changed my thinking was realizing that it could be mapped on to the
existing jointype "semi join". We still need one new concept,
"outer semi join" or whatever we decide to call it, but it's less of
a stretch than I'd supposed originally.)

regards, tom lane

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

#63Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#57)
Re: Performance improvement for joins where outer side is unique

On Fri, Mar 11, 2016 at 4:32 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

So I started re-reading this thread in preparation for looking at the
patch, and this bit in your initial message jumped out at me:

In all of our join algorithms in the executor, if the join type is SEMI,
we skip to the next outer row once we find a matching inner row. This is
because we don't want to allow duplicate rows in the inner side to
duplicate outer rows in the result set. Obviously this is required per SQL
spec. I believe we can also skip to the next outer row in this case when
we've managed to prove that no other row can possibly exist that matches
the current outer row, due to a unique index or group by/distinct clause
(for subqueries).

I wondered why, instead of inventing an extra semantics-modifying flag,
we couldn't just change the jointype to *be* JOIN_SEMI when we've
discovered that the inner side is unique.

Now of course this only works if the join type was INNER to start with.
If it was a LEFT join, you'd need an "outer semi join" jointype which
we haven't got at the moment. But I wonder whether inventing that
jointype wouldn't let us arrive at a less messy handling of things in
the executor and EXPLAIN. I'm not very enamored of plastering this
"match_first_tuple_only" flag on every join, in part because it doesn't
appear to have sensible semantics for other jointypes such as JOIN_RIGHT.
And I'd really be happier to see the information reflected by join type
than a new line in EXPLAIN, also.

The new join pushdown code in postgres_fdw does not grok SEMI and ANTI
joins because there is no straightforward way of reducing those back
to SQL. They can originate in multiple ways and not all of those can
be represented easily. I think it would be nice to do something to
fix this. For example, if a LEFT join WHERE outer_column IS NULL
turns into an ANTI join, it would be nice if that were marked in some
way so that postgres_fdw could conveniently emit it in the original
form.

Maybe the people who have been working on that patch just haven't been
creative enough in thinking about how to solve this problem, but it
makes me greet the idea of more join types that don't map directly
back to SQL with somewhat mixed feelings.

--
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

#64Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#63)
Re: Performance improvement for joins where outer side is unique

Robert Haas <robertmhaas@gmail.com> writes:

The new join pushdown code in postgres_fdw does not grok SEMI and ANTI
joins because there is no straightforward way of reducing those back
to SQL. They can originate in multiple ways and not all of those can
be represented easily. I think it would be nice to do something to
fix this. For example, if a LEFT join WHERE outer_column IS NULL
turns into an ANTI join, it would be nice if that were marked in some
way so that postgres_fdw could conveniently emit it in the original
form.

Maybe the people who have been working on that patch just haven't been
creative enough in thinking about how to solve this problem, but it
makes me greet the idea of more join types that don't map directly
back to SQL with somewhat mixed feelings.

I can't summon a whole lot of sympathy for that objection. These cases
won't arise with postgres_fdw as it stands because we'd never be able to
prove uniqueness on a foreign table. When and if someone tries to improve
that, we can think about how the whole thing ought to map to FDWs.

Having said that, your point does add a bit of weight to David's
suggestion of inventing two new join-type codes rather than overloading
JOIN_SEMI. I'd still be a bit inclined to display JOIN_INNER_UNIQUE or
whatever we call it as a "Semi" join in EXPLAIN, though, just to minimize
the amount of newness there.

regards, tom lane

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

#65David G. Johnston
david.g.johnston@gmail.com
In reply to: Tom Lane (#61)
Re: Performance improvement for joins where outer side is unique

On Saturday, March 12, 2016, Tom Lane <tgl@sss.pgh.pa.us> wrote:

"David G. Johnston" <david.g.johnston@gmail.com <javascript:;>> writes:

Don't the semantics of a SEMI JOIN also state that the output columns

only

come from the outer relation? i.e., the inner relation doesn't contribute
either rows or columns to the final result? Or is that simply
an implementation artifact of the fact that the only current way to

perform

a semi-join explicitly is via exists/in?

I think it's an artifact. What nodes.h actually says about it is you get
the values of one randomly-selected matching inner row, which seems like
a fine definition for the purposes we plan to put it to.

But is it a definition that actually materializes anywhere presently?

I'm not sure what we consider an authoritative source but relational
algebra does define the results of semi and anti joins as only containing
rows from main relation.

https://en.m.wikipedia.org/wiki/Relational_algebra

Given that this is largely internals (aside from the plan explanations
themselves) I guess we can punt for now but calling an inner or outer join
a semijoin in this case relies on a non-standard definition of semijoin -
namely that it is an optimized variation of the other joins instead of a
join type in its own right. This is complicated further in that we
do implement a true semijoin (using exists) while we allow for an anti join
to be non-standard if expressed using "left join ... is null" instead of
via "not exists".

Calling these optimizations outer/inner+semi/anti preserves the ability to
distinguish these versions from the standard definitions. I do like ithe
idea of it being exposed and encapsulated as a distinct join type instead
of being an attribute.

David J.

#66David G. Johnston
david.g.johnston@gmail.com
In reply to: David G. Johnston (#65)
Re: Performance improvement for joins where outer side is unique

On Saturday, March 12, 2016, David G. Johnston <david.g.johnston@gmail.com>
wrote:

On Saturday, March 12, 2016, Tom Lane <tgl@sss.pgh.pa.us
<javascript:_e(%7B%7D,'cvml','tgl@sss.pgh.pa.us');>> wrote:

"David G. Johnston" <david.g.johnston@gmail.com> writes:

Don't the semantics of a SEMI JOIN also state that the output columns

only

come from the outer relation? i.e., the inner relation doesn't

contribute

either rows or columns to the final result? Or is that simply
an implementation artifact of the fact that the only current way to

perform

a semi-join explicitly is via exists/in?

I think it's an artifact. What nodes.h actually says about it is you get
the values of one randomly-selected matching inner row, which seems like
a fine definition for the purposes we plan to put it to.

But is it a definition that actually materializes anywhere presently?

I'm not sure what we consider an authoritative source but relational
algebra does define the results of semi and anti joins as only containing
rows from main relation.

https://en.m.wikipedia.org/wiki/Relational_algebra

Pondering it more calling these optimizations "semi" joins further
distances us from the meaning of "semi" as used in relational algebra. The
half that semi refers to IS that only one half of the tables are returned.
That you only get a single row of output regardless of multiple potential
matches is simply a consequence of this and general set theory.

In short "semi" communicates a semantic meaning as to the intended output
of the query irrespective of the data upon which it is executed. We now
are hijacking the and calling something "semi" if by some chance the data
the query is operating against happens to be accommodating to some
particular optimization.

This seems wrong on definitional and cleanliness grounds.

So while I'm still liking the idea of introducing specializations of outer
and inner joins I think calling them "semi" joins adds a definitional
inconsistency we are better off avoiding.

This came about because calling something "outer semi join" struck me as
odd.

Something like "outer only join" and "inner only join" comes to mind.
Consider the parallel between this and "index only scan". Learning that
"only" means "join the outer row to the (at most for outer) one and only
row in the inner relation" doesn't seem to much of a challenge.

David J.

#67David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#58)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 12 March 2016 at 11:43, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I wrote:

I wondered why, instead of inventing an extra semantics-modifying flag,
we couldn't just change the jointype to *be* JOIN_SEMI when we've
discovered that the inner side is unique.

BTW, to clarify: I'm not imagining that we'd make this change in the
query jointree, as for example prepjointree.c might do. That would appear
to add join order constraints, which we don't want. But I'm thinking that
at the instant where we form a join Path, we could change the Path's
jointype to be JOIN_SEMI or JOIN_SEMI_OUTER if we're able to prove the
inner side unique, rather than annotating the Path with a separate flag.
Then that representation is what propagates forward.

I've attached a patch which implements this, although I did call the
new join type JOIN_LEFT_UNIQUE rather than JOIN_SEMI_OUTER.
I'm not all that confident that I've not added handling for
JOIN_LEFT_UNIQUE in places where it's not possible to get that join
type, I did leave out a good number of places where I know it's not
possible. I'm also not sure with too much certainty that I've got all
cases correct, but the regression tests pass. The patch is more
intended for assisting discussion than as a ready to commit patch.

It seems like the major intellectual complexity here is to figure out
how to detect inner-side-unique at reasonable cost. I see that for
LEFT joins you're caching that in the SpecialJoinInfos, which is probably
fine. But for INNER joins it looks like you're just doing it over again
for every candidate join, and that seems mighty expensive.

I have a rough idea for this, but I need to think of it a bit more to
make sure it's bulletproof.
I also just noticed (since it's been a while since I wrote the patch)
that in add_paths_to_joinrel() that innerrel can naturally be a
joinrel too, and we can fail to find uniqueness in that joinrel. I
think it should be possible to analyse the join rel too and search for
a base rel which supports the distinctness, and then ensure none of
the other rels which make up the join rel can cause tuple duplication
of that rel. But this just causes missed optimisation opportunities.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_82d2a07_2016-03-14.patchapplication/octet-stream; name=unique_joins_82d2a07_2016-03-14.patchDownload
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index e6d4cdb..e955dda 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -1068,6 +1068,7 @@ get_jointype_name(JoinType jointype)
 		case JOIN_INNER:
 			return "INNER";
 
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_LEFT:
 			return "LEFT";
 
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 40bffd6..cd91afa 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -3373,7 +3373,8 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 	 * not considered right now.
 	 */
 	if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
-		jointype != JOIN_RIGHT && jointype != JOIN_FULL)
+		jointype != JOIN_RIGHT && jointype != JOIN_FULL &&
+		jointype != JOIN_LEFT_UNIQUE)
 		return false;
 
 	/*
@@ -3506,6 +3507,7 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 			break;
 
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
 										  list_copy(fpinfo_i->remote_conds));
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index ee13136..72b9143 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1098,6 +1098,9 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					case JOIN_LEFT:
 						jointype = "Left";
 						break;
+					case JOIN_LEFT_UNIQUE:
+						jointype = "Left Unique";
+						break;
 					case JOIN_FULL:
 						jointype = "Full";
 						break;
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 369e666..f8c07de 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semi or unique left join then we'll consider
+					 * returning the first match, but after that we're done
+					 * with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI ||
+						node->js.jointype == JOIN_LEFT_UNIQUE)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -502,6 +504,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 		case JOIN_SEMI:
 			break;
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_ANTI:
 			hjstate->hj_NullInnerTupleSlot =
 				ExecInitNullTupleSlot(estate,
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 6db09b8..c321ac5 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,12 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semi or unique left join then we'll consider
+					 * returning the first match, but after that we're done
+					 * with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI ||
+						node->js.jointype == JOIN_LEFT_UNIQUE)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1559,6 +1561,7 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 			mergestate->mj_FillInner = false;
 			break;
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_ANTI:
 			mergestate->mj_FillOuter = true;
 			mergestate->mj_FillInner = false;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 555fa09..822703a 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -182,6 +182,7 @@ ExecNestLoop(NestLoopState *node)
 
 			if (!node->nl_MatchedOuter &&
 				(node->js.jointype == JOIN_LEFT ||
+				 node->js.jointype == JOIN_LEFT_UNIQUE ||
 				 node->js.jointype == JOIN_ANTI))
 			{
 				/*
@@ -247,10 +248,11 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * In a semi or unique left join then we'll consider returning the
+			 * first match, but after that we're done with this outer tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.jointype == JOIN_SEMI ||
+				node->js.jointype == JOIN_LEFT_UNIQUE)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -358,6 +360,7 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 		case JOIN_SEMI:
 			break;
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_ANTI:
 			nlstate->nl_NullInnerTupleSlot =
 				ExecInitNullTupleSlot(estate,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index df7c2fa..1f4b77e 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2063,6 +2063,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(is_unique_join);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index b9c3959..f374c36 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -838,6 +838,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(is_unique_join);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index eb0fc1e..e9d4b25 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2264,6 +2264,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(is_unique_join);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 5350329..a68bc70 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1928,7 +1928,9 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI ||
+		jointype == JOIN_LEFT_UNIQUE)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -1999,7 +2001,9 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI ||
+		path->jointype == JOIN_LEFT_UNIQUE)
 	{
 		/*
 		 * SEMI or ANTI join: executor will stop after first match.
@@ -2232,7 +2236,8 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerendsel = cache->leftendsel;
 		}
 		if (jointype == JOIN_LEFT ||
-			jointype == JOIN_ANTI)
+			jointype == JOIN_ANTI ||
+			jointype == JOIN_LEFT_UNIQUE)
 		{
 			outerstartsel = 0.0;
 			outerendsel = 1.0;
@@ -2878,7 +2883,9 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		path->jpath.jointype == JOIN_LEFT_UNIQUE)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
@@ -3958,6 +3965,7 @@ calc_joinrel_size_estimate(PlannerInfo *root,
 			nrows = outer_rows * inner_rows * jselec;
 			break;
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 			nrows = outer_rows * inner_rows * jselec;
 			if (nrows < outer_rows)
 				nrows = outer_rows;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 3b898da..cb46ea8 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -50,7 +51,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+										   RelOptInfo *innerrel);
 
 /*
  * add_paths_to_joinrel
@@ -88,6 +90,50 @@ add_paths_to_joinrel(PlannerInfo *root,
 	bool		mergejoin_allowed = true;
 	ListCell   *lc;
 
+	if (bms_membership(innerrel->relids) == BMS_SINGLETON)
+	{
+		/* left joins were already analyzed for uniqueness in mark_unique_joins() */
+		if (jointype == JOIN_LEFT)
+		{
+			if (sjinfo->is_unique_join)
+				jointype = JOIN_LEFT_UNIQUE;
+		}
+		else if (jointype == JOIN_INNER &&
+				 restrictlist != NIL &&
+				 rel_supports_distinctness(root, innerrel))
+		{
+			/*
+			 * Remember the number of items that were in the restrictlist as
+			 * the call to relation_has_unique_index_for may add more items
+			 * which we'll need to remove later.
+			 */
+			int org_len = list_length(restrictlist);
+
+			/*
+			 * rel_is_distinct_for requires restrict infos to have the correct
+			 * clause direction info
+			 */
+			foreach(lc, restrictlist)
+			{
+				clause_sides_match_join((RestrictInfo *) lfirst(lc),
+					outerrel, innerrel);
+			}
+
+			if (rel_is_distinct_for(root, innerrel, restrictlist))
+			{
+				/*
+				 * We can safely adjust the jointype to a semi join since this
+				 * innerrel can't produce more than one tuple for any single
+				 * outer tuple.
+				 */
+				jointype = JOIN_SEMI;
+			}
+
+			/* Remove any list items added by rel_is_distinct_for */
+			list_truncate(restrictlist, org_len);
+		}
+	}
+
 	extra.restrictlist = restrictlist;
 	extra.mergeclause_list = NIL;
 	extra.sjinfo = sjinfo;
@@ -836,6 +882,7 @@ match_unsorted_outer(PlannerInfo *root,
 	{
 		case JOIN_INNER:
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_SEMI:
 		case JOIN_ANTI:
 			nestjoinOK = true;
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 01d4fea..e76cd13 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -493,7 +493,8 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 			 * this SJ must be a LEFT join (not SEMI or ANTI, and certainly
 			 * not FULL) and the proposed join must not overlap the LHS.
 			 */
-			if (sjinfo->jointype != JOIN_LEFT ||
+			if ((sjinfo->jointype != JOIN_LEFT &&
+				 sjinfo->jointype != JOIN_LEFT_UNIQUE) ||
 				bms_overlap(joinrelids, sjinfo->min_lefthand))
 				return false;	/* invalid join path */
 
@@ -518,7 +519,8 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 	 */
 	if (must_be_leftjoin &&
 		(match_sjinfo == NULL ||
-		 match_sjinfo->jointype != JOIN_LEFT ||
+		 (match_sjinfo->jointype != JOIN_LEFT &&
+		  match_sjinfo->jointype != JOIN_LEFT_UNIQUE) ||
 		 !match_sjinfo->lhs_strict))
 		return false;			/* invalid join path */
 
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index f7f5714..f3206d6 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -34,11 +34,37 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+					  SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+/*
+ * mark_unique_joins
+ *		Analyze joins in order to determine if their inner side is unique based
+ *		on the join condition.
+ */
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs that have not already
+		 * been marked as unique by a previous call.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT &&
+			!sjinfo->is_unique_join &&
+			specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -92,6 +118,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to mark some joins as unique which we could
+		 * not do before
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -152,17 +184,17 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must not duplicate its outer side and must be a non-delaying left
+	 * join to a single baserel, else we aren't going to be able to do anything
+	 * with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->is_unique_join ||
+		sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -171,38 +203,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	/* Must be true as is_unique_join can only be set to true for base rels */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -213,7 +215,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -254,6 +257,44 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most 1 inner row for any single outer row. False is returned if there's
+ *		insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than 1 relation involved then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -275,10 +316,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we can't
+			 * mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -301,71 +340,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -560,6 +537,126 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list, callers should either
+ * make a copy of the list or trim it back to it's original length after
+ * calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *)lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *)rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *)get_rightop(rinfo->clause);
+			else
+				var = (Var *)get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false; /* can't prove rel to be distinct over clause_list */
+}
+
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness
+		 * by reference to unique indexes.  If there are no indexes then
+		 * there's certainly no unique indexes so there's nothing to prove
+		 * uniqueness on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 64bc8a8..5fe0e95 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1130,6 +1130,7 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	sjinfo->is_unique_join = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 443e64e..885746b 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -185,6 +185,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Analyze joins to find out which ones have a unique inner side */
+	mark_unique_joins(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index aa2c308..a50e1da 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -1598,6 +1598,7 @@ set_join_references(PlannerInfo *root, Join *join, int rtoffset)
 	switch (join->jointype)
 	{
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_SEMI:
 		case JOIN_ANTI:
 			inner_itlist->has_non_vars = false;
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 4d9febe..fd76cc6 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -990,7 +990,9 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		 * NB: this coding relies on the fact that list_concat is not
 		 * destructive to its second argument.
 		 */
-		lateral_ok = (j->jointype == JOIN_INNER || j->jointype == JOIN_LEFT);
+		lateral_ok = (j->jointype == JOIN_INNER ||
+					  j->jointype == JOIN_LEFT ||
+					  j->jointype == JOIN_LEFT_UNIQUE);
 		setNamespaceLateralState(l_namespace, true, lateral_ok);
 
 		sv_namespace_length = list_length(pstate->p_namespace);
@@ -1351,6 +1353,7 @@ buildMergedJoinVar(ParseState *pstate, JoinType jointype,
 				res_node = l_node;
 			break;
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 			/* Always use left var */
 			res_node = l_node;
 			break;
diff --git a/src/backend/utils/adt/network_selfuncs.c b/src/backend/utils/adt/network_selfuncs.c
index 2e39687..2551d28 100644
--- a/src/backend/utils/adt/network_selfuncs.c
+++ b/src/backend/utils/adt/network_selfuncs.c
@@ -216,6 +216,7 @@ networkjoinsel(PG_FUNCTION_ARGS)
 	{
 		case JOIN_INNER:
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_FULL:
 
 			/*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 490a090..d4615a8 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9063,6 +9063,7 @@ get_from_clause_item(Node *jtnode, Query *query, deparse_context *context)
 										 PRETTYINDENT_JOIN);
 				break;
 			case JOIN_LEFT:
+			case JOIN_LEFT_UNIQUE:
 				appendContextKeyword(context, " LEFT JOIN ",
 									 -PRETTYINDENT_STD,
 									 PRETTYINDENT_STD,
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index d396ef1..9d45ba5 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -2202,6 +2202,7 @@ eqjoinsel(PG_FUNCTION_ARGS)
 	{
 		case JOIN_INNER:
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_FULL:
 			selec = eqjoinsel_inner(operator, &vardata1, &vardata2);
 			break;
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index fad9988..4783b95 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -636,6 +636,14 @@ typedef enum JoinType
 	JOIN_ANTI,					/* 1 copy of each LHS row that has no match */
 
 	/*
+	 * The following join types are varients of other types for when the inner
+	 * side of the join is known to be unique. These serve soley as an
+	 * optimization to allow the executor to save looking for another matching
+	 * tuple in the inner side, when it's known that another cannot exist.
+	 */
+	JOIN_LEFT_UNIQUE,
+
+	/*
 	 * These codes are used internally in the planner, but are not supported
 	 * by the executor (nor, indeed, by most of the planner).
 	 */
@@ -664,6 +672,7 @@ typedef enum JoinType
 #define IS_OUTER_JOIN(jointype) \
 	(((1 << (jointype)) & \
 	  ((1 << JOIN_LEFT) | \
+	   (1 << JOIN_LEFT_UNIQUE) | \
 	   (1 << JOIN_FULL) | \
 	   (1 << JOIN_RIGHT) | \
 	   (1 << JOIN_ANTI))) != 0)
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 641728b..bf902da 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1722,6 +1722,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 596ffb3..1d8639b 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -98,7 +98,11 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+								List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 601bdb4..23781c4 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -884,29 +884,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Semi Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Semi Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 0391b8e..aca998c 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -186,7 +186,7 @@ explain (costs off)
   select * from ec1, ec2 where ff = x1 and x1 = '42'::int8alias2;
                QUERY PLAN                
 -----------------------------------------
- Nested Loop
+ Nested Loop Semi Join
    ->  Seq Scan on ec2
          Filter: (x1 = '42'::int8alias2)
    ->  Index Scan using ec1_pkey on ec1
@@ -310,7 +310,7 @@ explain (costs off)
          ->  Index Scan using ec1_expr3 on ec1 ec1_5
          ->  Index Scan using ec1_expr4 on ec1 ec1_6
    ->  Materialize
-         ->  Merge Join
+         ->  Merge Semi Join
                Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
                ->  Merge Append
                      Sort Key: (((ec1_1.ff + 2) + 1))
@@ -365,7 +365,7 @@ explain (costs off)
   where ss1.x = ec1.f1 and ec1.ff = 42::int8;
                      QUERY PLAN                      
 -----------------------------------------------------
- Merge Join
+ Merge Semi Join
    Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
    ->  Merge Append
          Sort Key: (((ec1_1.ff + 2) + 1))
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index cafbc5e..4ca87d1 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2756,8 +2756,8 @@ from nt3 as nt3
 where nt3.id = 1 and ss2.b3;
                   QUERY PLAN                   
 -----------------------------------------------
- Nested Loop
-   ->  Nested Loop
+ Nested Loop Semi Join
+   ->  Nested Loop Semi Join
          ->  Index Scan using nt3_pkey on nt3
                Index Cond: (id = 1)
          ->  Index Scan using nt2_pkey on nt2
@@ -4035,7 +4035,7 @@ explain (costs off)
     on (p.k = ss.k);
            QUERY PLAN            
 ---------------------------------
- Hash Left Join
+ Hash Left Unique Join
    Hash Cond: (p.k = c.k)
    ->  Seq Scan on parent p
    ->  Hash
@@ -5260,3 +5260,235 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique.
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Semi Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Semi Join
+   Output: j1.id, j3.id
+   Hash Cond: (j3.id = j1.id)
+   ->  Seq Scan on public.j3
+         Output: j3.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Unique Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Unique Join
+   Output: j1.id, j2.id
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- clauseless cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(8 rows)
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Semi Join
+   Output: j1.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop Semi Join
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(14 rows)
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop Semi Join
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(7 rows)
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop Semi Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 4aaa88f..35357e1 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -276,7 +276,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle);
 EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
                      QUERY PLAN                     
 ----------------------------------------------------
- Nested Loop
+ Nested Loop Semi Join
    ->  Subquery Scan on document
          Filter: f_leak(document.dtitle)
          ->  Seq Scan on document document_1
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 7f57526..496eb62 100644
--- a/src/test/regress/expected/select_views.out
+++ b/src/test/regress/expected/select_views.out
@@ -1411,7 +1411,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Semi Join
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1432,7 +1432,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Semi Join
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1466,7 +1466,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Semi Join
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1497,7 +1497,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Semi Join
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 3430f91..bb86c10 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1696,3 +1696,93 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique.
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- Ensure join is marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- Ensure join not marked as unique when not using =
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- clauseless cross joins can't be proved unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause uniquifies the join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure no unique joins when not all columns which are part of
+-- the unique index are part of the join clause.
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure unique joins work with multiple columns
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#68Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#67)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

On 12 March 2016 at 11:43, Tom Lane <tgl@sss.pgh.pa.us> wrote:

It seems like the major intellectual complexity here is to figure out
how to detect inner-side-unique at reasonable cost. I see that for
LEFT joins you're caching that in the SpecialJoinInfos, which is probably
fine. But for INNER joins it looks like you're just doing it over again
for every candidate join, and that seems mighty expensive.

I have a rough idea for this, but I need to think of it a bit more to
make sure it's bulletproof.

Where are we on this? I had left it alone for awhile because I supposed
that you were going to post a new patch, but it's been a couple weeks...

regards, tom lane

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

#69David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#68)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 2 April 2016 at 05:52, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

On 12 March 2016 at 11:43, Tom Lane <tgl@sss.pgh.pa.us> wrote:

It seems like the major intellectual complexity here is to figure out
how to detect inner-side-unique at reasonable cost. I see that for
LEFT joins you're caching that in the SpecialJoinInfos, which is probably
fine. But for INNER joins it looks like you're just doing it over again
for every candidate join, and that seems mighty expensive.

I have a rough idea for this, but I need to think of it a bit more to
make sure it's bulletproof.

Where are we on this? I had left it alone for awhile because I supposed
that you were going to post a new patch, but it's been a couple weeks...

Apologies for the delay. I was fully booked.

I worked on this today to try and get it into shape.

Changes:

* Added unique_rels and non_unique_rels cache to PlannerInfo. These
are both arrays of Lists which are indexed by the relid, which is
either proved to be unique by, or proved not to be unique by each
listed set of relations. Each List item is a Bitmapset containing the
relids of the relation which proved the uniqueness, or proved no
possibility of uniqueness for the non_unique_rels case. The
non_unique_rels cache seems far less useful than the unique one, as
with the standard join search, we start at level one, comparing
singleton relations and works our way up, so it's only towards the end
of the search that we'll have more rels on each side of the join
search. Many of the unique cases are found early on in the search, but
the non-unique cases must be rechecked as more relations are added to
the search, as that introduces a new possibility of uniqueness. In
fact, the only cache hit of the non-unique case in the regression
tests is a test that's using the GEQO, so I'm not quite sure if the
standard join search will ever have cache hit here. Never-the-less I
kept the cache, as it's likely going to be most useful to have it when
the GEQO is running the show, as that's when we're going to see the
largest number of relations to join.

There's also a small quirk which could lead to false negatives for
uniqueness which might still need ironed out: I don't yet attempt to
get the minimum set of relations which proved the uniqueness. This,
perhaps, does not matter much for the standard join search, but likely
will, more so for GEQO cases. Fixing this will require changes to
relation_has_unique_index_for() to get it to record and return the
relids of the clauses which matched the index.

* Renamed JOIN_LEFT_UNIQUE to JOIN_SEMI_LEFT. It seems better to
maintain the SEMI part. I thought about renaming JOIN_SEMI to
JOIN_SEMI_INNER, but thought I'd better not touch that here.

* Made a pass to update comments in the areas where I've added
handling for JOIN_SEMI_LEFT.

* I also updated comments in the regression tests to remove reference
to unique joins. "Join conversion" seems to be a better term now.

There was also a couple of places where I wasn't quite sure how to
handle JOIN_SEMI_LEFT. I've marked these with an XXX comment. Perhaps
how to handle them is more clear to you.

I also was not quite sure the best place to allocate memory for these
caches. I've ended up doing that in setup_simple_rel_arrays(), which
is likely the wrong spot. I originally did this in
standard_join_search(), after join_rel_level is allocated memory, but
I soon realised that it can't happen here as the GEQO requires the
caches too, and naturally it does not come through
standard_join_search().

I was not quite sure if I should update any docs to mention that Inner
Joins can become Semi Joins in some cases.

I was also a bit unsure if I should move the two new functions I added
to joinpath.c into analyzejoins.c.

Thanks for taking an interest in this.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_6f34aa1_2016-04-02.patchapplication/octet-stream; name=unique_joins_6f34aa1_2016-04-02.patchDownload
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 4fbbde1..5504091 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -3923,10 +3923,12 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 	/*
 	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
 	 * Constructing queries representing SEMI and ANTI joins is hard, hence
-	 * not considered right now.
+	 * not considered right now. SEMI_LEFT joins are ok here, since they're
+	 * merely an optimization of LEFT joins.
 	 */
 	if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
-		jointype != JOIN_RIGHT && jointype != JOIN_FULL)
+		jointype != JOIN_RIGHT && jointype != JOIN_FULL &&
+		jointype != JOIN_SEMI_LEFT)
 		return false;
 
 	/*
@@ -4059,6 +4061,7 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 			break;
 
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:
 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
 										  list_copy(fpinfo_i->remote_conds));
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 09c2304..7c114ca 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1121,6 +1121,9 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					case JOIN_LEFT:
 						jointype = "Left";
 						break;
+					case JOIN_SEMI_LEFT:
+						jointype = "Semi Left";
+						break;
 					case JOIN_FULL:
 						jointype = "Full";
 						break;
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 369e666..1436a5b 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semi or semi left join, we'll consider returning
+					 * the first match, but after that we're done with this
+					 * outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI ||
+						node->js.jointype == JOIN_SEMI_LEFT)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -502,6 +504,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 		case JOIN_SEMI:
 			break;
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:
 		case JOIN_ANTI:
 			hjstate->hj_NullInnerTupleSlot =
 				ExecInitNullTupleSlot(estate,
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 6db09b8..f61a8da 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,12 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semi or semi left join, we'll consider returning
+					 * the first match, but after that we're done with this
+					 * outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI ||
+						node->js.jointype == JOIN_SEMI_LEFT)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1559,6 +1561,7 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 			mergestate->mj_FillInner = false;
 			break;
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:
 		case JOIN_ANTI:
 			mergestate->mj_FillOuter = true;
 			mergestate->mj_FillInner = false;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 555fa09..ebcf0f2 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -182,6 +182,7 @@ ExecNestLoop(NestLoopState *node)
 
 			if (!node->nl_MatchedOuter &&
 				(node->js.jointype == JOIN_LEFT ||
+				 node->js.jointype == JOIN_SEMI_LEFT ||
 				 node->js.jointype == JOIN_ANTI))
 			{
 				/*
@@ -247,10 +248,11 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * In a semi or semi left join, we'll consider returning the first
+			 * match, but after that we're done with this outer tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.jointype == JOIN_SEMI ||
+				node->js.jointype == JOIN_SEMI_LEFT)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -358,6 +360,7 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 		case JOIN_SEMI:
 			break;
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:
 		case JOIN_ANTI:
 			nlstate->nl_NullInnerTupleSlot =
 				ExecInitNullTupleSlot(estate,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index f4e4a91..005d290 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2067,6 +2067,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(is_unique_join);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 854c062..4a87efd 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -839,6 +839,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(is_unique_join);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index bfd12ac..f34567b 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2268,6 +2268,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(is_unique_join);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index b395751..8656629 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1893,8 +1893,8 @@ cost_group(Path *path, PlannerInfo *root,
  * estimate and getting a tight lower bound.  We choose to not examine the
  * join quals here, since that's by far the most expensive part of the
  * calculations.  The end result is that CPU-cost considerations must be
- * left for the second phase; and for SEMI/ANTI joins, we must also postpone
- * incorporation of the inner path's run cost.
+ * left for the second phase; and for SEMI, SEMI_LEFT and ANTI joins, we must
+ * also postpone incorporation of the inner path's run cost.
  *
  * 'workspace' is to be filled with startup_cost, total_cost, and perhaps
  *		other data to be used by final_cost_nestloop
@@ -1902,7 +1902,7 @@ cost_group(Path *path, PlannerInfo *root,
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
  * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'semifactors' contains valid data if jointype is SEMI, SEMI_LEFT or ANTI
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
@@ -1940,10 +1940,12 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_SEMI_LEFT ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * SEMI, SEMI_LEFT or ANTI join: executor will stop after first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -1977,7 +1979,8 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
  * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'semifactors' contains valid data if path->jointype is SEMI, SEMI_LEFT or
+ * ANTI
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
@@ -2017,10 +2020,12 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_SEMI_LEFT ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * SEMI, SEMI_LEFT or ANTI join: executor will stop after first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2250,6 +2255,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerendsel = cache->leftendsel;
 		}
 		if (jointype == JOIN_LEFT ||
+			jointype == JOIN_SEMI_LEFT ||
 			jointype == JOIN_ANTI)
 		{
 			outerstartsel = 0.0;
@@ -2773,7 +2779,8 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
  * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'semifactors' contains valid data if path->jointype is SEMI, SEMI_LEFT or
+ * ANTI
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
@@ -2896,13 +2903,15 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_SEMI_LEFT ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * SEMI, SEMI_LEFT or ANTI join: executor will stop after first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
@@ -2937,10 +2946,10 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 			clamp_row_est(inner_path_rows / virtualbuckets) * 0.05;
 
 		/* Get # of tuples that will pass the basic join */
-		if (path->jpath.jointype == JOIN_SEMI)
-			hashjointuples = outer_matched_rows;
-		else
+		if (path->jpath.jointype == JOIN_ANTI)
 			hashjointuples = outer_path_rows - outer_matched_rows;
+		else
+			hashjointuples = outer_matched_rows;
 	}
 	else
 	{
@@ -3469,11 +3478,11 @@ get_restriction_qual_cost(PlannerInfo *root, RelOptInfo *baserel,
 
 /*
  * compute_semi_anti_join_factors
- *	  Estimate how much of the inner input a SEMI or ANTI join
+ *	  Estimate how much of the inner input a SEMI, SEMI_LEFT or ANTI join
  *	  can be expected to scan.
  *
- * In a hash or nestloop SEMI/ANTI join, the executor will stop scanning
- * inner rows as soon as it finds a match to the current outer row.
+ * In a hash or nestloop SEMI/SEMI_LEFT/ANTI join, the executor will stop
+ * scanning inner rows as soon as it finds a match to the current outer row.
  * We should therefore adjust some of the cost components for this effect.
  * This function computes some estimates needed for these adjustments.
  * These estimates will be the same regardless of the particular paths used
@@ -3483,7 +3492,7 @@ get_restriction_qual_cost(PlannerInfo *root, RelOptInfo *baserel,
  * Input parameters:
  *	outerrel: outer relation under consideration
  *	innerrel: inner relation under consideration
- *	jointype: must be JOIN_SEMI or JOIN_ANTI
+ *	jointype: must be JOIN_SEMI, JOIN_SEMI_LEFT or JOIN_ANTI
  *	sjinfo: SpecialJoinInfo relevant to this join
  *	restrictlist: join quals
  * Output parameters:
@@ -3506,7 +3515,9 @@ compute_semi_anti_join_factors(PlannerInfo *root,
 	ListCell   *l;
 
 	/* Should only be called in these cases */
-	Assert(jointype == JOIN_SEMI || jointype == JOIN_ANTI);
+	Assert(jointype == JOIN_SEMI ||
+		   jointype == JOIN_SEMI_LEFT ||
+		   jointype == JOIN_ANTI);
 
 	/*
 	 * In an ANTI join, we must ignore clauses that are "pushed down", since
@@ -3530,7 +3541,8 @@ compute_semi_anti_join_factors(PlannerInfo *root,
 		joinquals = restrictlist;
 
 	/*
-	 * Get the JOIN_SEMI or JOIN_ANTI selectivity of the join clauses.
+	 * Get the JOIN_SEMI, JOIN_SEMI_LEFT or JOIN_ANTI selectivity of the join
+	 * clauses.
 	 */
 	jselec = clauselist_selectivity(root,
 									joinquals,
@@ -3969,6 +3981,10 @@ calc_joinrel_size_estimate(PlannerInfo *root,
 	 *
 	 * For JOIN_SEMI and JOIN_ANTI, the selectivity is defined as the fraction
 	 * of LHS rows that have matches, and we apply that straightforwardly.
+	 *
+	 * For JOIN_SEMI_LEFT, the selectivity is defined as the fraction of the
+	 * LHS rows that have matches, although unlike JOIN_SEMI we must consider
+	 * NULL RHS rows, and take the higher estimate of the two.
 	 */
 	switch (jointype)
 	{
@@ -3993,6 +4009,12 @@ calc_joinrel_size_estimate(PlannerInfo *root,
 			nrows = outer_rows * jselec;
 			/* pselec not used */
 			break;
+		case JOIN_SEMI_LEFT:
+			nrows = outer_rows * jselec;
+			if (nrows < outer_rows)
+				nrows = outer_rows;
+			nrows *= pselec;
+			break;
 		case JOIN_ANTI:
 			nrows = outer_rows * (1.0 - jselec);
 			nrows *= pselec;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index f3aced3..cfdda7b 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -28,6 +29,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 bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
+static JoinType get_optimal_jointype(PlannerInfo *root,
+					 RelOptInfo *outerrel,
+					 RelOptInfo *innerrel,
+					 JoinType jointype,
+					 SpecialJoinInfo *sjinfo,
+					 List *restrictlist);
 static void sort_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 JoinType jointype, JoinPathExtraData *extra);
@@ -50,7 +61,176 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+						RelOptInfo *innerrel);
 
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	bool		is_unique;
+	int			org_len;
+	ListCell   *lc;
+
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Remember the number of items that were in the restrictlist as the call
+	 * to relation_has_unique_index_for may add more items which we'll need to
+	 * remove later.
+	 */
+	org_len = list_length(restrictlist);
+
+	/*
+	 * rel_is_distinct_for requires restrict infos to have the correct clause
+	 * direction info
+	 */
+	foreach(lc, restrictlist)
+	{
+		clause_sides_match_join((RestrictInfo *) lfirst(lc), outerrel,
+								innerrel);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	is_unique = rel_is_distinct_for(root, innerrel, restrictlist);
+
+	/* Remove any list items added by rel_is_distinct_for */
+	list_truncate(restrictlist, org_len);
+
+	return is_unique;
+}
+
+/*
+ * get_optimal_jointype
+ *	  We may be able to optimize some joins by converting the JoinType to one
+ *	  which the executor is able to run more efficiently. Here we look for
+ *	  such cases and if we find a better choice, then we'll return it,
+ *	  otherwise we'll return the original JoinType.
+ */
+static JoinType
+get_optimal_jointype(PlannerInfo *root,
+					 RelOptInfo *outerrel,
+					 RelOptInfo *innerrel,
+					 JoinType jointype,
+					 SpecialJoinInfo *sjinfo,
+					 List *restrictlist)
+{
+	int			innerrelid;
+
+	/*
+	 * LEFT JOINs in which we have proved the inner side to be unique can be
+	 * converted into JOIN_SEMI_LEFT.
+	 */
+	if (jointype == JOIN_LEFT)
+	{
+		if (sjinfo->is_unique_join)
+			return JOIN_SEMI_LEFT;
+		else
+			return JOIN_LEFT;
+	}
+
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return jointype;
+
+	/*
+	 * Any INNER JOINs which can be proven to return at most one inner tuple
+	 * for each outer tuple can be converted in to a JOIN_SEMI.
+	 */
+	if (jointype == JOIN_INNER)
+	{
+		MemoryContext old_context;
+		ListCell   *lc;
+
+		/* can't optimize jointype with an empty restrictlist */
+		if (restrictlist == NIL)
+			return jointype;
+
+		/*
+		 * First let's query the unique and non-unique caches to see if we've
+		 * managed to prove that innerrel is unique for some subset of this
+		 * outerrel. We don't need an exact match, as if we have any extra
+		 * outerrels than were previously cached, then they can't make the
+		 * innerrel any less unique.
+		 */
+		foreach(lc, root->unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(unique_rels, outerrel->relids))
+			{
+				/* ensure is_innerrel_unique_for() agrees */
+				Assert(is_innerrel_unique_for(root, outerrel, innerrel,
+											  restrictlist));
+
+				return JOIN_SEMI;		/* Success! */
+			}
+		}
+
+		/*
+		 * We may have previously determined that this outerrel, or some
+		 * superset thereof, cannot prove this innerrel to be unique.
+		 */
+		foreach(lc, root->non_unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(outerrel->relids, unique_rels))
+			{
+				/* ensure is_innerrel_unique_for() agrees */
+				Assert(!is_innerrel_unique_for(root, outerrel, innerrel,
+											   restrictlist));
+
+				return jointype;
+			}
+		}
+
+		/*
+		 * We may be getting called from the geqo and might not be working in
+		 * the standard planner's memory context, so let's ensure we are.
+		 */
+		old_context = MemoryContextSwitchTo(root->planner_cxt);
+
+		if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+		{
+			/*
+			 * XXX Should we attempt to get the minimum set of outerrels which
+			 * proved this innerrel to be unique? If we did this then we might
+			 * be able to make a few more cases unique that we otherwise
+			 * couldn't. However, the standard join search always start with
+			 * fewer rels anyway, so this may not matter, although perhaps we
+			 * should be more aggressive to make this work better with the
+			 * geqo?
+			 */
+
+			/* cache the result for next time */
+			root->unique_rels[innerrelid] =
+				lappend(root->unique_rels[innerrelid], outerrel->relids);
+
+			jointype = JOIN_SEMI;		/* Success! */
+		}
+		else
+		{
+			/*
+			 * None of outerrel helped prove innerrel unique, so we can safely
+			 * reject this rel, or a subset of this rel in future checks.
+			 */
+			root->non_unique_rels[innerrelid] =
+				lappend(root->non_unique_rels[innerrelid], outerrel->relids);
+		}
+
+		MemoryContextSwitchTo(old_context);
+	}
+	return jointype;
+}
 
 /*
  * add_paths_to_joinrel
@@ -88,6 +268,13 @@ add_paths_to_joinrel(PlannerInfo *root,
 	bool		mergejoin_allowed = true;
 	ListCell   *lc;
 
+	/*
+	 * There may be a more optimal JoinType to use. Check for such cases
+	 * first.
+	 */
+	jointype = get_optimal_jointype(root, outerrel, innerrel, jointype, sjinfo,
+									restrictlist);
+
 	extra.restrictlist = restrictlist;
 	extra.mergeclause_list = NIL;
 	extra.sjinfo = sjinfo;
@@ -109,10 +296,12 @@ add_paths_to_joinrel(PlannerInfo *root,
 														  &mergejoin_allowed);
 
 	/*
-	 * If it's SEMI or ANTI join, compute correction factors for cost
-	 * estimation.  These will be the same for all paths.
+	 * If it's SEMI, SEMI_LEFT or ANTI join, compute correction factors for
+	 * cost estimation.  These will be the same for all paths.
 	 */
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_SEMI_LEFT ||
+		jointype == JOIN_ANTI)
 		compute_semi_anti_join_factors(root, outerrel, innerrel,
 									   jointype, sjinfo, restrictlist,
 									   &extra.semifactors);
@@ -827,16 +1016,17 @@ match_unsorted_outer(PlannerInfo *root,
 	ListCell   *lc1;
 
 	/*
-	 * Nestloop only supports inner, left, semi, and anti joins.  Also, if we
-	 * are doing a right or full mergejoin, we must use *all* the mergeclauses
-	 * as join clauses, else we will not have a valid plan.  (Although these
-	 * two flags are currently inverses, keep them separate for clarity and
-	 * possible future changes.)
+	 * Nestloop only supports inner, left, semi_left, semi, and anti joins.
+	 * Also, if we are doing a right or full mergejoin, we must use *all* the
+	 * mergeclauses as join clauses, else we will not have a valid plan.
+	 * (Although these two flags are currently inverses, keep them separate
+	 * for clarity and possible future changes.)
 	 */
 	switch (jointype)
 	{
 		case JOIN_INNER:
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:
 		case JOIN_SEMI:
 		case JOIN_ANTI:
 			nestjoinOK = true;
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 01d4fea..b91cb79 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -490,10 +490,12 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 			/*
 			 * The proposed join could still be legal, but only if we're
 			 * allowed to associate it into the RHS of this SJ.  That means
-			 * this SJ must be a LEFT join (not SEMI or ANTI, and certainly
-			 * not FULL) and the proposed join must not overlap the LHS.
+			 * this SJ must be a LEFT or SEMI_LEFT join (not SEMI or ANTI, and
+			 * certainly not FULL) and the proposed join must not overlap the
+			 * LHS.
 			 */
-			if (sjinfo->jointype != JOIN_LEFT ||
+			if ((sjinfo->jointype != JOIN_LEFT &&
+				 sjinfo->jointype != JOIN_SEMI_LEFT) ||
 				bms_overlap(joinrelids, sjinfo->min_lefthand))
 				return false;	/* invalid join path */
 
@@ -508,8 +510,8 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 	}
 
 	/*
-	 * Fail if violated any SJ's RHS and didn't match to a LEFT SJ: the
-	 * proposed join can't associate into an SJ's RHS.
+	 * Fail if violated any SJ's RHS and didn't match to a LEFT or SEMI_LEFT
+	 * SJ: the proposed join can't associate into an SJ's RHS.
 	 *
 	 * Also, fail if the proposed join's predicate isn't strict; we're
 	 * essentially checking to see if we can apply outer-join identity 3, and
@@ -518,7 +520,8 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 	 */
 	if (must_be_leftjoin &&
 		(match_sjinfo == NULL ||
-		 match_sjinfo->jointype != JOIN_LEFT ||
+		 (match_sjinfo->jointype != JOIN_LEFT &&
+		  match_sjinfo->jointype != JOIN_SEMI_LEFT) ||
 		 !match_sjinfo->lhs_strict))
 		return false;			/* invalid join path */
 
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index f7f5714..7446eb5 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -34,11 +34,37 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+						   SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+/*
+ * mark_unique_joins
+ *		Analyze joins in order to determine if their inner side is unique based
+ *		on the join condition.
+ */
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs that have not already
+		 * been marked as unique by a previous call.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT &&
+			!sjinfo->is_unique_join &&
+			specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -92,6 +118,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to mark some joins as unique which we could not
+		 * do before
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -152,17 +184,17 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must not duplicate its outer side and must be a non-delaying left
+	 * join to a single baserel, else we aren't going to be able to do
+	 * anything with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->is_unique_join ||
+		sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -171,38 +203,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	/* Must be true as is_unique_join can only be set to true for base rels */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -213,7 +215,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -254,6 +257,44 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most one inner row for any single outer row. False is returned if
+ *		there's insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than one relation involved, then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -275,10 +316,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we
+			 * can't mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -301,71 +340,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -560,6 +537,127 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list, callers should either
+ * make a copy of the list or trim it back to it's original length after
+ * calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int			relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *) rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *) get_rightop(rinfo->clause);
+			else
+				var = (Var *) get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false;				/* can't prove rel to be distinct over
+								 * clause_list */
+}
+
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness by
+		 * reference to unique indexes.  If there are no indexes then there's
+		 * certainly no unique indexes so there's nothing to prove uniqueness
+		 * on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query	   *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 9999eea..e90f358 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1130,6 +1130,7 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	sjinfo->is_unique_join = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 88d7ea4..efaa655 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -185,6 +185,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Analyze joins to find out which ones have a unique inner side */
+	mark_unique_joins(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index dd2b9ed..fe6f789 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -1617,6 +1617,7 @@ set_join_references(PlannerInfo *root, Join *join, int rtoffset)
 	switch (join->jointype)
 	{
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:
 		case JOIN_SEMI:
 		case JOIN_ANTI:
 			inner_itlist->has_non_vars = false;
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 6f24b03..f1aeb9b 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -71,6 +71,14 @@ setup_simple_rel_arrays(PlannerInfo *root)
 	/* simple_rte_array is an array equivalent of the rtable list */
 	root->simple_rte_array = (RangeTblEntry **)
 		palloc0(root->simple_rel_array_size * sizeof(RangeTblEntry *));
+
+	/* initialize the unique relation caches */
+	root->unique_rels = (List **)
+		palloc0(root->simple_rel_array_size * sizeof(List *));
+
+	root->non_unique_rels = (List **)
+		palloc0(root->simple_rel_array_size * sizeof(List *));
+
 	rti = 1;
 	foreach(lc, root->parse->rtable)
 	{
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index c9edd88..28c2db0 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -978,11 +978,11 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		/*
 		 * Make the left-side RTEs available for LATERAL access within the
 		 * right side, by temporarily adding them to the pstate's namespace
-		 * list.  Per SQL:2008, if the join type is not INNER or LEFT then the
-		 * left-side names must still be exposed, but it's an error to
-		 * reference them.  (Stupid design, but that's what it says.)  Hence,
-		 * we always push them into the namespace, but mark them as not
-		 * lateral_ok if the jointype is wrong.
+		 * list.  Per SQL:2008, if the join type is not INNER, LEFT or
+		 * SEMI_LEFT  then the left-side names must still be exposed, but it's
+		 * an error to reference them.  (Stupid design, but that's what it
+		 * says.)  Hence, we always push them into the namespace, but mark
+		 * them as not lateral_ok if the jointype is wrong.
 		 *
 		 * Notice that we don't require the merged namespace list to be
 		 * conflict-free.  See the comments for scanNameSpaceForRefname().
@@ -990,7 +990,9 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		 * NB: this coding relies on the fact that list_concat is not
 		 * destructive to its second argument.
 		 */
-		lateral_ok = (j->jointype == JOIN_INNER || j->jointype == JOIN_LEFT);
+		lateral_ok = (j->jointype == JOIN_INNER ||
+					  j->jointype == JOIN_LEFT ||
+					  j->jointype == JOIN_SEMI_LEFT);
 		setNamespaceLateralState(l_namespace, true, lateral_ok);
 
 		sv_namespace_length = list_length(pstate->p_namespace);
diff --git a/src/backend/utils/adt/network_selfuncs.c b/src/backend/utils/adt/network_selfuncs.c
index 2e39687..4250325 100644
--- a/src/backend/utils/adt/network_selfuncs.c
+++ b/src/backend/utils/adt/network_selfuncs.c
@@ -216,6 +216,7 @@ networkjoinsel(PG_FUNCTION_ARGS)
 	{
 		case JOIN_INNER:
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:	/* XXX belongs here, or with SEMI/ANTI? */
 		case JOIN_FULL:
 
 			/*
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index a6555e9..68aedfa 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -2202,6 +2202,7 @@ eqjoinsel(PG_FUNCTION_ARGS)
 	{
 		case JOIN_INNER:
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT: /* XXX belongs here, or with SEMI/ANTI? */
 		case JOIN_FULL:
 			selec = eqjoinsel_inner(operator, &vardata1, &vardata2);
 			break;
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 734df77..87695b1 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -638,6 +638,14 @@ typedef enum JoinType
 	JOIN_ANTI,					/* 1 copy of each LHS row that has no match */
 
 	/*
+	 * The following join type is a variant of JOIN_LEFT for when the inner
+	 * side of the join is known to be unique. This serves solely as an
+	 * optimization to allow the executor to skip looking for another matching
+	 * tuple in the inner side, when it's known that another cannot exist.
+	 */
+	JOIN_SEMI_LEFT,
+
+	/*
 	 * These codes are used internally in the planner, but are not supported
 	 * by the executor (nor, indeed, by most of the planner).
 	 */
@@ -666,6 +674,7 @@ typedef enum JoinType
 #define IS_OUTER_JOIN(jointype) \
 	(((1 << (jointype)) & \
 	  ((1 << JOIN_LEFT) | \
+	   (1 << JOIN_SEMI_LEFT) | \
 	   (1 << JOIN_FULL) | \
 	   (1 << JOIN_RIGHT) | \
 	   (1 << JOIN_ANTI))) != 0)
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index d39c73b..85ee975 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -221,6 +221,18 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to optimize joins to try to prove
+	 * their inner side to be unique based on the join condition. This is a
+	 * rather expensive thing to do as it requires checking each relations
+	 * unique indexes to see if the relation can, at most, return one tuple
+	 * for each outer tuple. We use this cache during the join search to
+	 * record lists of the sets of relations which both prove, and disprove
+	 * the uniqueness properties for the relid indexed by these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1729,6 +1741,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 1f96e27..3c0dcbd 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -98,7 +98,11 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+								List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 3ff6691..952833f 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -880,29 +880,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Semi Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Semi Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 0391b8e..aca998c 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -186,7 +186,7 @@ explain (costs off)
   select * from ec1, ec2 where ff = x1 and x1 = '42'::int8alias2;
                QUERY PLAN                
 -----------------------------------------
- Nested Loop
+ Nested Loop Semi Join
    ->  Seq Scan on ec2
          Filter: (x1 = '42'::int8alias2)
    ->  Index Scan using ec1_pkey on ec1
@@ -310,7 +310,7 @@ explain (costs off)
          ->  Index Scan using ec1_expr3 on ec1 ec1_5
          ->  Index Scan using ec1_expr4 on ec1 ec1_6
    ->  Materialize
-         ->  Merge Join
+         ->  Merge Semi Join
                Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
                ->  Merge Append
                      Sort Key: (((ec1_1.ff + 2) + 1))
@@ -365,7 +365,7 @@ explain (costs off)
   where ss1.x = ec1.f1 and ec1.ff = 42::int8;
                      QUERY PLAN                      
 -----------------------------------------------------
- Merge Join
+ Merge Semi Join
    Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
    ->  Merge Append
          Sort Key: (((ec1_1.ff + 2) + 1))
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index cafbc5e..2e867fa 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2756,8 +2756,8 @@ from nt3 as nt3
 where nt3.id = 1 and ss2.b3;
                   QUERY PLAN                   
 -----------------------------------------------
- Nested Loop
-   ->  Nested Loop
+ Nested Loop Semi Join
+   ->  Nested Loop Semi Join
          ->  Index Scan using nt3_pkey on nt3
                Index Cond: (id = 1)
          ->  Index Scan using nt2_pkey on nt2
@@ -4035,7 +4035,7 @@ explain (costs off)
     on (p.k = ss.k);
            QUERY PLAN            
 ---------------------------------
- Hash Left Join
+ Hash Semi Left Join
    Hash Cond: (p.k = c.k)
    ->  Seq Scan on parent p
    ->  Hash
@@ -5260,3 +5260,238 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to change joins into their appropriate semi join
+-- type
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Semi Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Semi Join
+   Output: j1.id, j3.id
+   Hash Cond: (j3.id = j1.id)
+   ->  Seq Scan on public.j3
+         Output: j3.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Semi Left Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Semi Left Join
+   Output: j1.id, j2.id
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(8 rows)
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Semi Join
+   Output: j1.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop Semi Join
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(14 rows)
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop Semi Join
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of join conversions
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(7 rows)
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop Semi Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 067aa8d..3f0c705 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -276,7 +276,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle);
 EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
                      QUERY PLAN                     
 ----------------------------------------------------
- Nested Loop
+ Nested Loop Semi Join
    ->  Subquery Scan on document
          Filter: f_leak(document.dtitle)
          ->  Seq Scan on document document_1
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 7f57526..496eb62 100644
--- a/src/test/regress/expected/select_views.out
+++ b/src/test/regress/expected/select_views.out
@@ -1411,7 +1411,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Semi Join
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1432,7 +1432,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Semi Join
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1466,7 +1466,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Semi Join
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1497,7 +1497,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Semi Join
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 3430f91..38cc39c 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1696,3 +1696,96 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to change joins into their appropriate semi join
+-- type
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of join conversions
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#70David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#69)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 2 April 2016 at 23:26, David Rowley <david.rowley@2ndquadrant.com> wrote:

I worked on this today to try and get it into shape.

In the last patch I failed to notice that there's an alternative
expected results file for one of the regression tests.

The attached patch includes the fix to update that file to match the
new expected EXPLAIN output.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_57837dc_2016-04-03.patchapplication/octet-stream; name=unique_joins_57837dc_2016-04-03.patchDownload
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 4fbbde1..5504091 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -3923,10 +3923,12 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 	/*
 	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
 	 * Constructing queries representing SEMI and ANTI joins is hard, hence
-	 * not considered right now.
+	 * not considered right now. SEMI_LEFT joins are ok here, since they're
+	 * merely an optimization of LEFT joins.
 	 */
 	if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
-		jointype != JOIN_RIGHT && jointype != JOIN_FULL)
+		jointype != JOIN_RIGHT && jointype != JOIN_FULL &&
+		jointype != JOIN_SEMI_LEFT)
 		return false;
 
 	/*
@@ -4059,6 +4061,7 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 			break;
 
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:
 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
 										  list_copy(fpinfo_i->remote_conds));
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 09c2304..7c114ca 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1121,6 +1121,9 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					case JOIN_LEFT:
 						jointype = "Left";
 						break;
+					case JOIN_SEMI_LEFT:
+						jointype = "Semi Left";
+						break;
 					case JOIN_FULL:
 						jointype = "Full";
 						break;
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 369e666..1436a5b 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semi or semi left join, we'll consider returning
+					 * the first match, but after that we're done with this
+					 * outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI ||
+						node->js.jointype == JOIN_SEMI_LEFT)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -502,6 +504,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 		case JOIN_SEMI:
 			break;
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:
 		case JOIN_ANTI:
 			hjstate->hj_NullInnerTupleSlot =
 				ExecInitNullTupleSlot(estate,
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 6db09b8..f61a8da 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,12 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In a semi or semi left join, we'll consider returning
+					 * the first match, but after that we're done with this
+					 * outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI ||
+						node->js.jointype == JOIN_SEMI_LEFT)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1559,6 +1561,7 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 			mergestate->mj_FillInner = false;
 			break;
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:
 		case JOIN_ANTI:
 			mergestate->mj_FillOuter = true;
 			mergestate->mj_FillInner = false;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 555fa09..ebcf0f2 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -182,6 +182,7 @@ ExecNestLoop(NestLoopState *node)
 
 			if (!node->nl_MatchedOuter &&
 				(node->js.jointype == JOIN_LEFT ||
+				 node->js.jointype == JOIN_SEMI_LEFT ||
 				 node->js.jointype == JOIN_ANTI))
 			{
 				/*
@@ -247,10 +248,11 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * In a semi or semi left join, we'll consider returning the first
+			 * match, but after that we're done with this outer tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.jointype == JOIN_SEMI ||
+				node->js.jointype == JOIN_SEMI_LEFT)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -358,6 +360,7 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 		case JOIN_SEMI:
 			break;
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:
 		case JOIN_ANTI:
 			nlstate->nl_NullInnerTupleSlot =
 				ExecInitNullTupleSlot(estate,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index f4e4a91..005d290 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2067,6 +2067,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(is_unique_join);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 854c062..4a87efd 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -839,6 +839,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(is_unique_join);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index bfd12ac..f34567b 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2268,6 +2268,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(is_unique_join);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index b395751..8656629 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1893,8 +1893,8 @@ cost_group(Path *path, PlannerInfo *root,
  * estimate and getting a tight lower bound.  We choose to not examine the
  * join quals here, since that's by far the most expensive part of the
  * calculations.  The end result is that CPU-cost considerations must be
- * left for the second phase; and for SEMI/ANTI joins, we must also postpone
- * incorporation of the inner path's run cost.
+ * left for the second phase; and for SEMI, SEMI_LEFT and ANTI joins, we must
+ * also postpone incorporation of the inner path's run cost.
  *
  * 'workspace' is to be filled with startup_cost, total_cost, and perhaps
  *		other data to be used by final_cost_nestloop
@@ -1902,7 +1902,7 @@ cost_group(Path *path, PlannerInfo *root,
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
  * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'semifactors' contains valid data if jointype is SEMI, SEMI_LEFT or ANTI
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
@@ -1940,10 +1940,12 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_SEMI_LEFT ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * SEMI, SEMI_LEFT or ANTI join: executor will stop after first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -1977,7 +1979,8 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
  * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'semifactors' contains valid data if path->jointype is SEMI, SEMI_LEFT or
+ * ANTI
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
@@ -2017,10 +2020,12 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_SEMI_LEFT ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * SEMI, SEMI_LEFT or ANTI join: executor will stop after first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2250,6 +2255,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerendsel = cache->leftendsel;
 		}
 		if (jointype == JOIN_LEFT ||
+			jointype == JOIN_SEMI_LEFT ||
 			jointype == JOIN_ANTI)
 		{
 			outerstartsel = 0.0;
@@ -2773,7 +2779,8 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
  * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'semifactors' contains valid data if path->jointype is SEMI, SEMI_LEFT or
+ * ANTI
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
@@ -2896,13 +2903,15 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_SEMI_LEFT ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * SEMI, SEMI_LEFT or ANTI join: executor will stop after first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
@@ -2937,10 +2946,10 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 			clamp_row_est(inner_path_rows / virtualbuckets) * 0.05;
 
 		/* Get # of tuples that will pass the basic join */
-		if (path->jpath.jointype == JOIN_SEMI)
-			hashjointuples = outer_matched_rows;
-		else
+		if (path->jpath.jointype == JOIN_ANTI)
 			hashjointuples = outer_path_rows - outer_matched_rows;
+		else
+			hashjointuples = outer_matched_rows;
 	}
 	else
 	{
@@ -3469,11 +3478,11 @@ get_restriction_qual_cost(PlannerInfo *root, RelOptInfo *baserel,
 
 /*
  * compute_semi_anti_join_factors
- *	  Estimate how much of the inner input a SEMI or ANTI join
+ *	  Estimate how much of the inner input a SEMI, SEMI_LEFT or ANTI join
  *	  can be expected to scan.
  *
- * In a hash or nestloop SEMI/ANTI join, the executor will stop scanning
- * inner rows as soon as it finds a match to the current outer row.
+ * In a hash or nestloop SEMI/SEMI_LEFT/ANTI join, the executor will stop
+ * scanning inner rows as soon as it finds a match to the current outer row.
  * We should therefore adjust some of the cost components for this effect.
  * This function computes some estimates needed for these adjustments.
  * These estimates will be the same regardless of the particular paths used
@@ -3483,7 +3492,7 @@ get_restriction_qual_cost(PlannerInfo *root, RelOptInfo *baserel,
  * Input parameters:
  *	outerrel: outer relation under consideration
  *	innerrel: inner relation under consideration
- *	jointype: must be JOIN_SEMI or JOIN_ANTI
+ *	jointype: must be JOIN_SEMI, JOIN_SEMI_LEFT or JOIN_ANTI
  *	sjinfo: SpecialJoinInfo relevant to this join
  *	restrictlist: join quals
  * Output parameters:
@@ -3506,7 +3515,9 @@ compute_semi_anti_join_factors(PlannerInfo *root,
 	ListCell   *l;
 
 	/* Should only be called in these cases */
-	Assert(jointype == JOIN_SEMI || jointype == JOIN_ANTI);
+	Assert(jointype == JOIN_SEMI ||
+		   jointype == JOIN_SEMI_LEFT ||
+		   jointype == JOIN_ANTI);
 
 	/*
 	 * In an ANTI join, we must ignore clauses that are "pushed down", since
@@ -3530,7 +3541,8 @@ compute_semi_anti_join_factors(PlannerInfo *root,
 		joinquals = restrictlist;
 
 	/*
-	 * Get the JOIN_SEMI or JOIN_ANTI selectivity of the join clauses.
+	 * Get the JOIN_SEMI, JOIN_SEMI_LEFT or JOIN_ANTI selectivity of the join
+	 * clauses.
 	 */
 	jselec = clauselist_selectivity(root,
 									joinquals,
@@ -3969,6 +3981,10 @@ calc_joinrel_size_estimate(PlannerInfo *root,
 	 *
 	 * For JOIN_SEMI and JOIN_ANTI, the selectivity is defined as the fraction
 	 * of LHS rows that have matches, and we apply that straightforwardly.
+	 *
+	 * For JOIN_SEMI_LEFT, the selectivity is defined as the fraction of the
+	 * LHS rows that have matches, although unlike JOIN_SEMI we must consider
+	 * NULL RHS rows, and take the higher estimate of the two.
 	 */
 	switch (jointype)
 	{
@@ -3993,6 +4009,12 @@ calc_joinrel_size_estimate(PlannerInfo *root,
 			nrows = outer_rows * jselec;
 			/* pselec not used */
 			break;
+		case JOIN_SEMI_LEFT:
+			nrows = outer_rows * jselec;
+			if (nrows < outer_rows)
+				nrows = outer_rows;
+			nrows *= pselec;
+			break;
 		case JOIN_ANTI:
 			nrows = outer_rows * (1.0 - jselec);
 			nrows *= pselec;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index f3aced3..cfdda7b 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -28,6 +29,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 bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
+static JoinType get_optimal_jointype(PlannerInfo *root,
+					 RelOptInfo *outerrel,
+					 RelOptInfo *innerrel,
+					 JoinType jointype,
+					 SpecialJoinInfo *sjinfo,
+					 List *restrictlist);
 static void sort_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 JoinType jointype, JoinPathExtraData *extra);
@@ -50,7 +61,176 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+						RelOptInfo *innerrel);
 
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	bool		is_unique;
+	int			org_len;
+	ListCell   *lc;
+
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Remember the number of items that were in the restrictlist as the call
+	 * to relation_has_unique_index_for may add more items which we'll need to
+	 * remove later.
+	 */
+	org_len = list_length(restrictlist);
+
+	/*
+	 * rel_is_distinct_for requires restrict infos to have the correct clause
+	 * direction info
+	 */
+	foreach(lc, restrictlist)
+	{
+		clause_sides_match_join((RestrictInfo *) lfirst(lc), outerrel,
+								innerrel);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	is_unique = rel_is_distinct_for(root, innerrel, restrictlist);
+
+	/* Remove any list items added by rel_is_distinct_for */
+	list_truncate(restrictlist, org_len);
+
+	return is_unique;
+}
+
+/*
+ * get_optimal_jointype
+ *	  We may be able to optimize some joins by converting the JoinType to one
+ *	  which the executor is able to run more efficiently. Here we look for
+ *	  such cases and if we find a better choice, then we'll return it,
+ *	  otherwise we'll return the original JoinType.
+ */
+static JoinType
+get_optimal_jointype(PlannerInfo *root,
+					 RelOptInfo *outerrel,
+					 RelOptInfo *innerrel,
+					 JoinType jointype,
+					 SpecialJoinInfo *sjinfo,
+					 List *restrictlist)
+{
+	int			innerrelid;
+
+	/*
+	 * LEFT JOINs in which we have proved the inner side to be unique can be
+	 * converted into JOIN_SEMI_LEFT.
+	 */
+	if (jointype == JOIN_LEFT)
+	{
+		if (sjinfo->is_unique_join)
+			return JOIN_SEMI_LEFT;
+		else
+			return JOIN_LEFT;
+	}
+
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return jointype;
+
+	/*
+	 * Any INNER JOINs which can be proven to return at most one inner tuple
+	 * for each outer tuple can be converted in to a JOIN_SEMI.
+	 */
+	if (jointype == JOIN_INNER)
+	{
+		MemoryContext old_context;
+		ListCell   *lc;
+
+		/* can't optimize jointype with an empty restrictlist */
+		if (restrictlist == NIL)
+			return jointype;
+
+		/*
+		 * First let's query the unique and non-unique caches to see if we've
+		 * managed to prove that innerrel is unique for some subset of this
+		 * outerrel. We don't need an exact match, as if we have any extra
+		 * outerrels than were previously cached, then they can't make the
+		 * innerrel any less unique.
+		 */
+		foreach(lc, root->unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(unique_rels, outerrel->relids))
+			{
+				/* ensure is_innerrel_unique_for() agrees */
+				Assert(is_innerrel_unique_for(root, outerrel, innerrel,
+											  restrictlist));
+
+				return JOIN_SEMI;		/* Success! */
+			}
+		}
+
+		/*
+		 * We may have previously determined that this outerrel, or some
+		 * superset thereof, cannot prove this innerrel to be unique.
+		 */
+		foreach(lc, root->non_unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(outerrel->relids, unique_rels))
+			{
+				/* ensure is_innerrel_unique_for() agrees */
+				Assert(!is_innerrel_unique_for(root, outerrel, innerrel,
+											   restrictlist));
+
+				return jointype;
+			}
+		}
+
+		/*
+		 * We may be getting called from the geqo and might not be working in
+		 * the standard planner's memory context, so let's ensure we are.
+		 */
+		old_context = MemoryContextSwitchTo(root->planner_cxt);
+
+		if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+		{
+			/*
+			 * XXX Should we attempt to get the minimum set of outerrels which
+			 * proved this innerrel to be unique? If we did this then we might
+			 * be able to make a few more cases unique that we otherwise
+			 * couldn't. However, the standard join search always start with
+			 * fewer rels anyway, so this may not matter, although perhaps we
+			 * should be more aggressive to make this work better with the
+			 * geqo?
+			 */
+
+			/* cache the result for next time */
+			root->unique_rels[innerrelid] =
+				lappend(root->unique_rels[innerrelid], outerrel->relids);
+
+			jointype = JOIN_SEMI;		/* Success! */
+		}
+		else
+		{
+			/*
+			 * None of outerrel helped prove innerrel unique, so we can safely
+			 * reject this rel, or a subset of this rel in future checks.
+			 */
+			root->non_unique_rels[innerrelid] =
+				lappend(root->non_unique_rels[innerrelid], outerrel->relids);
+		}
+
+		MemoryContextSwitchTo(old_context);
+	}
+	return jointype;
+}
 
 /*
  * add_paths_to_joinrel
@@ -88,6 +268,13 @@ add_paths_to_joinrel(PlannerInfo *root,
 	bool		mergejoin_allowed = true;
 	ListCell   *lc;
 
+	/*
+	 * There may be a more optimal JoinType to use. Check for such cases
+	 * first.
+	 */
+	jointype = get_optimal_jointype(root, outerrel, innerrel, jointype, sjinfo,
+									restrictlist);
+
 	extra.restrictlist = restrictlist;
 	extra.mergeclause_list = NIL;
 	extra.sjinfo = sjinfo;
@@ -109,10 +296,12 @@ add_paths_to_joinrel(PlannerInfo *root,
 														  &mergejoin_allowed);
 
 	/*
-	 * If it's SEMI or ANTI join, compute correction factors for cost
-	 * estimation.  These will be the same for all paths.
+	 * If it's SEMI, SEMI_LEFT or ANTI join, compute correction factors for
+	 * cost estimation.  These will be the same for all paths.
 	 */
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_SEMI ||
+		jointype == JOIN_SEMI_LEFT ||
+		jointype == JOIN_ANTI)
 		compute_semi_anti_join_factors(root, outerrel, innerrel,
 									   jointype, sjinfo, restrictlist,
 									   &extra.semifactors);
@@ -827,16 +1016,17 @@ match_unsorted_outer(PlannerInfo *root,
 	ListCell   *lc1;
 
 	/*
-	 * Nestloop only supports inner, left, semi, and anti joins.  Also, if we
-	 * are doing a right or full mergejoin, we must use *all* the mergeclauses
-	 * as join clauses, else we will not have a valid plan.  (Although these
-	 * two flags are currently inverses, keep them separate for clarity and
-	 * possible future changes.)
+	 * Nestloop only supports inner, left, semi_left, semi, and anti joins.
+	 * Also, if we are doing a right or full mergejoin, we must use *all* the
+	 * mergeclauses as join clauses, else we will not have a valid plan.
+	 * (Although these two flags are currently inverses, keep them separate
+	 * for clarity and possible future changes.)
 	 */
 	switch (jointype)
 	{
 		case JOIN_INNER:
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:
 		case JOIN_SEMI:
 		case JOIN_ANTI:
 			nestjoinOK = true;
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 01d4fea..b91cb79 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -490,10 +490,12 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 			/*
 			 * The proposed join could still be legal, but only if we're
 			 * allowed to associate it into the RHS of this SJ.  That means
-			 * this SJ must be a LEFT join (not SEMI or ANTI, and certainly
-			 * not FULL) and the proposed join must not overlap the LHS.
+			 * this SJ must be a LEFT or SEMI_LEFT join (not SEMI or ANTI, and
+			 * certainly not FULL) and the proposed join must not overlap the
+			 * LHS.
 			 */
-			if (sjinfo->jointype != JOIN_LEFT ||
+			if ((sjinfo->jointype != JOIN_LEFT &&
+				 sjinfo->jointype != JOIN_SEMI_LEFT) ||
 				bms_overlap(joinrelids, sjinfo->min_lefthand))
 				return false;	/* invalid join path */
 
@@ -508,8 +510,8 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 	}
 
 	/*
-	 * Fail if violated any SJ's RHS and didn't match to a LEFT SJ: the
-	 * proposed join can't associate into an SJ's RHS.
+	 * Fail if violated any SJ's RHS and didn't match to a LEFT or SEMI_LEFT
+	 * SJ: the proposed join can't associate into an SJ's RHS.
 	 *
 	 * Also, fail if the proposed join's predicate isn't strict; we're
 	 * essentially checking to see if we can apply outer-join identity 3, and
@@ -518,7 +520,8 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 	 */
 	if (must_be_leftjoin &&
 		(match_sjinfo == NULL ||
-		 match_sjinfo->jointype != JOIN_LEFT ||
+		 (match_sjinfo->jointype != JOIN_LEFT &&
+		  match_sjinfo->jointype != JOIN_SEMI_LEFT) ||
 		 !match_sjinfo->lhs_strict))
 		return false;			/* invalid join path */
 
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index f7f5714..7446eb5 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -34,11 +34,37 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+						   SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
 
+/*
+ * mark_unique_joins
+ *		Analyze joins in order to determine if their inner side is unique based
+ *		on the join condition.
+ */
+void
+mark_unique_joins(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs that have not already
+		 * been marked as unique by a previous call.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT &&
+			!sjinfo->is_unique_join &&
+			specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->is_unique_join = true;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -92,6 +118,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to mark some joins as unique which we could not
+		 * do before
+		 */
+		mark_unique_joins(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -152,17 +184,17 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must not duplicate its outer side and must be a non-delaying left
+	 * join to a single baserel, else we aren't going to be able to do
+	 * anything with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->is_unique_join ||
+		sjinfo->jointype != JOIN_LEFT ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -171,38 +203,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	/* Must be true as is_unique_join can only be set to true for base rels */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -213,7 +215,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -254,6 +257,44 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most one inner row for any single outer row. False is returned if
+ *		there's insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than one relation involved, then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -275,10 +316,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we
+			 * can't mark the join as unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -301,71 +340,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -560,6 +537,127 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list, callers should either
+ * make a copy of the list or trim it back to it's original length after
+ * calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int			relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *) rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *) get_rightop(rinfo->clause);
+			else
+				var = (Var *) get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false;				/* can't prove rel to be distinct over
+								 * clause_list */
+}
+
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness by
+		 * reference to unique indexes.  If there are no indexes then there's
+		 * certainly no unique indexes so there's nothing to prove uniqueness
+		 * on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query	   *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 9999eea..e90f358 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1130,6 +1130,7 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	sjinfo->is_unique_join = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 88d7ea4..efaa655 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -185,6 +185,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Analyze joins to find out which ones have a unique inner side */
+	mark_unique_joins(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index dd2b9ed..fe6f789 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -1617,6 +1617,7 @@ set_join_references(PlannerInfo *root, Join *join, int rtoffset)
 	switch (join->jointype)
 	{
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:
 		case JOIN_SEMI:
 		case JOIN_ANTI:
 			inner_itlist->has_non_vars = false;
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 6f24b03..f1aeb9b 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -71,6 +71,14 @@ setup_simple_rel_arrays(PlannerInfo *root)
 	/* simple_rte_array is an array equivalent of the rtable list */
 	root->simple_rte_array = (RangeTblEntry **)
 		palloc0(root->simple_rel_array_size * sizeof(RangeTblEntry *));
+
+	/* initialize the unique relation caches */
+	root->unique_rels = (List **)
+		palloc0(root->simple_rel_array_size * sizeof(List *));
+
+	root->non_unique_rels = (List **)
+		palloc0(root->simple_rel_array_size * sizeof(List *));
+
 	rti = 1;
 	foreach(lc, root->parse->rtable)
 	{
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index c9edd88..28c2db0 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -978,11 +978,11 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		/*
 		 * Make the left-side RTEs available for LATERAL access within the
 		 * right side, by temporarily adding them to the pstate's namespace
-		 * list.  Per SQL:2008, if the join type is not INNER or LEFT then the
-		 * left-side names must still be exposed, but it's an error to
-		 * reference them.  (Stupid design, but that's what it says.)  Hence,
-		 * we always push them into the namespace, but mark them as not
-		 * lateral_ok if the jointype is wrong.
+		 * list.  Per SQL:2008, if the join type is not INNER, LEFT or
+		 * SEMI_LEFT  then the left-side names must still be exposed, but it's
+		 * an error to reference them.  (Stupid design, but that's what it
+		 * says.)  Hence, we always push them into the namespace, but mark
+		 * them as not lateral_ok if the jointype is wrong.
 		 *
 		 * Notice that we don't require the merged namespace list to be
 		 * conflict-free.  See the comments for scanNameSpaceForRefname().
@@ -990,7 +990,9 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		 * NB: this coding relies on the fact that list_concat is not
 		 * destructive to its second argument.
 		 */
-		lateral_ok = (j->jointype == JOIN_INNER || j->jointype == JOIN_LEFT);
+		lateral_ok = (j->jointype == JOIN_INNER ||
+					  j->jointype == JOIN_LEFT ||
+					  j->jointype == JOIN_SEMI_LEFT);
 		setNamespaceLateralState(l_namespace, true, lateral_ok);
 
 		sv_namespace_length = list_length(pstate->p_namespace);
diff --git a/src/backend/utils/adt/network_selfuncs.c b/src/backend/utils/adt/network_selfuncs.c
index 2e39687..4250325 100644
--- a/src/backend/utils/adt/network_selfuncs.c
+++ b/src/backend/utils/adt/network_selfuncs.c
@@ -216,6 +216,7 @@ networkjoinsel(PG_FUNCTION_ARGS)
 	{
 		case JOIN_INNER:
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT:	/* XXX belongs here, or with SEMI/ANTI? */
 		case JOIN_FULL:
 
 			/*
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index a6555e9..68aedfa 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -2202,6 +2202,7 @@ eqjoinsel(PG_FUNCTION_ARGS)
 	{
 		case JOIN_INNER:
 		case JOIN_LEFT:
+		case JOIN_SEMI_LEFT: /* XXX belongs here, or with SEMI/ANTI? */
 		case JOIN_FULL:
 			selec = eqjoinsel_inner(operator, &vardata1, &vardata2);
 			break;
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 734df77..87695b1 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -638,6 +638,14 @@ typedef enum JoinType
 	JOIN_ANTI,					/* 1 copy of each LHS row that has no match */
 
 	/*
+	 * The following join type is a variant of JOIN_LEFT for when the inner
+	 * side of the join is known to be unique. This serves solely as an
+	 * optimization to allow the executor to skip looking for another matching
+	 * tuple in the inner side, when it's known that another cannot exist.
+	 */
+	JOIN_SEMI_LEFT,
+
+	/*
 	 * These codes are used internally in the planner, but are not supported
 	 * by the executor (nor, indeed, by most of the planner).
 	 */
@@ -666,6 +674,7 @@ typedef enum JoinType
 #define IS_OUTER_JOIN(jointype) \
 	(((1 << (jointype)) & \
 	  ((1 << JOIN_LEFT) | \
+	   (1 << JOIN_SEMI_LEFT) | \
 	   (1 << JOIN_FULL) | \
 	   (1 << JOIN_RIGHT) | \
 	   (1 << JOIN_ANTI))) != 0)
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index d39c73b..85ee975 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -221,6 +221,18 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to optimize joins to try to prove
+	 * their inner side to be unique based on the join condition. This is a
+	 * rather expensive thing to do as it requires checking each relations
+	 * unique indexes to see if the relation can, at most, return one tuple
+	 * for each outer tuple. We use this cache during the join search to
+	 * record lists of the sets of relations which both prove, and disprove
+	 * the uniqueness properties for the relid indexed by these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1729,6 +1741,7 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		is_unique_join; /* matches a max of 1 row per outer join row */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 1f96e27..3c0dcbd 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -98,7 +98,11 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+								List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
 
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 3ff6691..952833f 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -880,29 +880,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Semi Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Semi Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 0391b8e..aca998c 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -186,7 +186,7 @@ explain (costs off)
   select * from ec1, ec2 where ff = x1 and x1 = '42'::int8alias2;
                QUERY PLAN                
 -----------------------------------------
- Nested Loop
+ Nested Loop Semi Join
    ->  Seq Scan on ec2
          Filter: (x1 = '42'::int8alias2)
    ->  Index Scan using ec1_pkey on ec1
@@ -310,7 +310,7 @@ explain (costs off)
          ->  Index Scan using ec1_expr3 on ec1 ec1_5
          ->  Index Scan using ec1_expr4 on ec1 ec1_6
    ->  Materialize
-         ->  Merge Join
+         ->  Merge Semi Join
                Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
                ->  Merge Append
                      Sort Key: (((ec1_1.ff + 2) + 1))
@@ -365,7 +365,7 @@ explain (costs off)
   where ss1.x = ec1.f1 and ec1.ff = 42::int8;
                      QUERY PLAN                      
 -----------------------------------------------------
- Merge Join
+ Merge Semi Join
    Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
    ->  Merge Append
          Sort Key: (((ec1_1.ff + 2) + 1))
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index cafbc5e..2e867fa 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2756,8 +2756,8 @@ from nt3 as nt3
 where nt3.id = 1 and ss2.b3;
                   QUERY PLAN                   
 -----------------------------------------------
- Nested Loop
-   ->  Nested Loop
+ Nested Loop Semi Join
+   ->  Nested Loop Semi Join
          ->  Index Scan using nt3_pkey on nt3
                Index Cond: (id = 1)
          ->  Index Scan using nt2_pkey on nt2
@@ -4035,7 +4035,7 @@ explain (costs off)
     on (p.k = ss.k);
            QUERY PLAN            
 ---------------------------------
- Hash Left Join
+ Hash Semi Left Join
    Hash Cond: (p.k = c.k)
    ->  Seq Scan on parent p
    ->  Hash
@@ -5260,3 +5260,238 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to change joins into their appropriate semi join
+-- type
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Semi Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Semi Join
+   Output: j1.id, j3.id
+   Hash Cond: (j3.id = j1.id)
+   ->  Seq Scan on public.j3
+         Output: j3.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Semi Left Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Semi Left Join
+   Output: j1.id, j2.id
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(8 rows)
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Semi Join
+   Output: j1.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop Semi Join
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(14 rows)
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop Semi Join
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of join conversions
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(7 rows)
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop Semi Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 067aa8d..3f0c705 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -276,7 +276,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle);
 EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
                      QUERY PLAN                     
 ----------------------------------------------------
- Nested Loop
+ Nested Loop Semi Join
    ->  Subquery Scan on document
          Filter: f_leak(document.dtitle)
          ->  Seq Scan on document document_1
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 7f57526..496eb62 100644
--- a/src/test/regress/expected/select_views.out
+++ b/src/test/regress/expected/select_views.out
@@ -1411,7 +1411,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Semi Join
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1432,7 +1432,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Semi Join
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1466,7 +1466,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Semi Join
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1497,7 +1497,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Semi Join
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/expected/select_views_1.out b/src/test/regress/expected/select_views_1.out
index 5275ef0..0c7338f 100644
--- a/src/test/regress/expected/select_views_1.out
+++ b/src/test/regress/expected/select_views_1.out
@@ -1411,7 +1411,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Semi Join
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1432,7 +1432,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Semi Join
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1466,7 +1466,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Semi Join
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1497,7 +1497,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Semi Join
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 3430f91..38cc39c 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1696,3 +1696,96 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to change joins into their appropriate semi join
+-- type
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of join conversions
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#71Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#70)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

In the last patch I failed to notice that there's an alternative
expected results file for one of the regression tests.
The attached patch includes the fix to update that file to match the
new expected EXPLAIN output.

Starting to look at this again. I wonder, now that you have the generic
caching mechanism for remembering whether join inner sides have been
proven unique, is it still worth having the is_unique_join field in
SpecialJoinInfo? It seems like that's creating a separate code path
for special joins vs. inner joins that may not be buying us much.
It does potentially save lookups in the unique_rels cache, if you already
have the SpecialJoinInfo at hand, but I'm not sure what that's worth.

Also, as I'm looking around at the planner some more, I'm beginning to get
uncomfortable with the idea of using JOIN_SEMI this way. It's fine so far
as the executor is concerned, no doubt, but there's a lot of planner
expectations about the use of JOIN_SEMI that we'd be violating. One is
that there should be a SpecialJoinInfo for any SEMI join. Another is that
JOIN_SEMI can be implemented by unique-ifying the inner side and then
doing a regular inner join; that's not a code path we wish to trigger in
these situations. The patch might avoid tripping over these hazards as it
stands, but it seems fragile, and third-party FDWs could easily contain
code that'll be broken. So I'm starting to feel that we'd better invent
two new JoinTypes after all, to make sure we can distinguish plain-join-
with-inner-side-known-unique from a real SEMI join when we need to.

What's your thoughts on these matters?

regards, tom lane

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

#72David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#71)
Re: Performance improvement for joins where outer side is unique

On 7 April 2016 at 04:05, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

In the last patch I failed to notice that there's an alternative
expected results file for one of the regression tests.
The attached patch includes the fix to update that file to match the
new expected EXPLAIN output.

Starting to look at this again. I wonder, now that you have the generic
caching mechanism for remembering whether join inner sides have been
proven unique, is it still worth having the is_unique_join field in
SpecialJoinInfo? It seems like that's creating a separate code path
for special joins vs. inner joins that may not be buying us much.
It does potentially save lookups in the unique_rels cache, if you already
have the SpecialJoinInfo at hand, but I'm not sure what that's worth.

I quite like that field where it is, as it should make
remove_useless_joins() a bit more efficient, as after a LEFT JOIN is
removed, the previous code would go off and try to make sure all the
joins are unique again, but now we cache that, and save it from having
to bother doing that again, on joins already marked as unique.

Certainly changing that would mean one less special case in
joinpath.c, as the JOIN_LEFT case can be handle the same as the other
cases, although it looks like probably, if I do change that, then I'd
probably move is_innerrel_unique_for() into analyzejoins.c, and put
the special case for JOIN_LEFT in that function, so that it calls
specialjoin_is_unique_join(), then cache the sjinfo->min_righthand in
the unique_rels cache if the result comes back positive, and in the
non_unique_rels cache if negative... But it seems a bit crazy to go to
the trouble or all that caching, when we can just throw the result in
a struct field in the case of Special Joins. Maybe we could just hide
both the new joinpath.c functions in analyzejoins.c and call it quits.
It's not as if there's no special cases for JOIN_LEFT in that file.

Also, as I'm looking around at the planner some more, I'm beginning to get
uncomfortable with the idea of using JOIN_SEMI this way. It's fine so far
as the executor is concerned, no doubt, but there's a lot of planner
expectations about the use of JOIN_SEMI that we'd be violating. One is
that there should be a SpecialJoinInfo for any SEMI join. Another is that
JOIN_SEMI can be implemented by unique-ifying the inner side and then
doing a regular inner join; that's not a code path we wish to trigger in
these situations. The patch might avoid tripping over these hazards as it
stands, but it seems fragile, and third-party FDWs could easily contain
code that'll be broken. So I'm starting to feel that we'd better invent
two new JoinTypes after all, to make sure we can distinguish plain-join-
with-inner-side-known-unique from a real SEMI join when we need to.

What's your thoughts on these matters?

I was a bit uncomfortable with JOIN_INNER becoming JOIN_SEMI before,
but that was for EXPLAIN reasons. So I do think it's better to have
JOIN_INNER_UNIQUE and JOIN_LEFT_UNIQUE instead. I can go make that
change, but unsure on how EXPLAIN will display these now. I'd need to
pull out my tests if we don't have anything to show in EXPLAIN, and
I'd really rather have tests for this.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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

#73David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#72)
Re: Performance improvement for joins where outer side is unique

On 7 April 2016 at 08:01, David Rowley <david.rowley@2ndquadrant.com> wrote:

On 7 April 2016 at 04:05, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Starting to look at this again. I wonder, now that you have the generic
caching mechanism for remembering whether join inner sides have been
proven unique, is it still worth having the is_unique_join field in
SpecialJoinInfo? It seems like that's creating a separate code path
for special joins vs. inner joins that may not be buying us much.
It does potentially save lookups in the unique_rels cache, if you already
have the SpecialJoinInfo at hand, but I'm not sure what that's worth.

I quite like that field where it is, as it should make
remove_useless_joins() a bit more efficient, as after a LEFT JOIN is
removed, the previous code would go off and try to make sure all the
joins are unique again, but now we cache that, and save it from having
to bother doing that again, on joins already marked as unique.

Certainly changing that would mean one less special case in
joinpath.c, as the JOIN_LEFT case can be handle the same as the other
cases, although it looks like probably, if I do change that, then I'd
probably move is_innerrel_unique_for() into analyzejoins.c, and put
the special case for JOIN_LEFT in that function, so that it calls
specialjoin_is_unique_join(), then cache the sjinfo->min_righthand in
the unique_rels cache if the result comes back positive, and in the
non_unique_rels cache if negative... But it seems a bit crazy to go to
the trouble or all that caching, when we can just throw the result in
a struct field in the case of Special Joins. Maybe we could just hide
both the new joinpath.c functions in analyzejoins.c and call it quits.
It's not as if there's no special cases for JOIN_LEFT in that file.

We could also get rid of the SpecialJoinInfo.is_unique_join and just
store this as optimal_jointype, where this would be initialised to
jointype in make_outerjoininfo(), and then set in mark_unique_joins().
This would simplify the test in get_optimal_jointype(), perhaps if
(IS_OUTER_JOIN(jointype)) return sjinfo->optimal_jointype;

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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

#74David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#72)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 7 April 2016 at 08:01, David Rowley <david.rowley@2ndquadrant.com> wrote:

On 7 April 2016 at 04:05, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Starting to look at this again. I wonder, now that you have the generic
caching mechanism for remembering whether join inner sides have been
proven unique, is it still worth having the is_unique_join field in
SpecialJoinInfo? It seems like that's creating a separate code path
for special joins vs. inner joins that may not be buying us much.
It does potentially save lookups in the unique_rels cache, if you already
have the SpecialJoinInfo at hand, but I'm not sure what that's worth.

I quite like that field where it is, as it should make
remove_useless_joins() a bit more efficient, as after a LEFT JOIN is
removed, the previous code would go off and try to make sure all the
joins are unique again, but now we cache that, and save it from having
to bother doing that again, on joins already marked as unique.

Certainly changing that would mean one less special case in
joinpath.c, as the JOIN_LEFT case can be handle the same as the other
cases, although it looks like probably, if I do change that, then I'd
probably move is_innerrel_unique_for() into analyzejoins.c, and put
the special case for JOIN_LEFT in that function, so that it calls
specialjoin_is_unique_join(), then cache the sjinfo->min_righthand in
the unique_rels cache if the result comes back positive, and in the
non_unique_rels cache if negative... But it seems a bit crazy to go to
the trouble or all that caching, when we can just throw the result in
a struct field in the case of Special Joins. Maybe we could just hide
both the new joinpath.c functions in analyzejoins.c and call it quits.
It's not as if there's no special cases for JOIN_LEFT in that file.

Also, as I'm looking around at the planner some more, I'm beginning to get
uncomfortable with the idea of using JOIN_SEMI this way. It's fine so far
as the executor is concerned, no doubt, but there's a lot of planner
expectations about the use of JOIN_SEMI that we'd be violating. One is
that there should be a SpecialJoinInfo for any SEMI join. Another is that
JOIN_SEMI can be implemented by unique-ifying the inner side and then
doing a regular inner join; that's not a code path we wish to trigger in
these situations. The patch might avoid tripping over these hazards as it
stands, but it seems fragile, and third-party FDWs could easily contain
code that'll be broken. So I'm starting to feel that we'd better invent
two new JoinTypes after all, to make sure we can distinguish plain-join-
with-inner-side-known-unique from a real SEMI join when we need to.

What's your thoughts on these matters?

I was a bit uncomfortable with JOIN_INNER becoming JOIN_SEMI before,
but that was for EXPLAIN reasons. So I do think it's better to have
JOIN_INNER_UNIQUE and JOIN_LEFT_UNIQUE instead. I can go make that
change, but unsure on how EXPLAIN will display these now. I'd need to
pull out my tests if we don't have anything to show in EXPLAIN, and
I'd really rather have tests for this.

I've attached an updated patch which introduces JOIN_INNER_UNIQUE and
JOIN_LEFT_UNIQUE. So unique inner joins no longer borrow JOIN_SEMI.

I also made some changes around the is_unique_join flag in
SpecialJoinInfo. I've changed this to become optimal_jointype, which
is initially set to jointype and updated by what used to be called
mark_unique_joins(), now called optimize_outerjoin_types(). The LEFT
JOIN removal code now only bothers with special joins with
optimal_jointype as JOIN_LEFT_UNIQUE. This still saves any double work
analyzing the unique properties of LEFT JOINs. I've moved the two new
functions I put in joinpath.c into analyzejoins.c

In EXPLAIN, I named these new join types "Unique Inner" and "Unique
Left". Going by a comment in explain.c, there's some historic reason
that we display "Hash Join" rather than "Hash Inner Join", and "Nested
Loop", rather than "Nested Loop Join" or "Nested Loop Inner Join". I
wasn't quite sure if Unique Inner Joins should use a similar shorthand
form, so I didn't touch that code. We'll get "Nested Loop Unique Inner
Join" now instead of "Nested Loop". I wasn't able to think of some
equivalent shorthand form that made sense.

I know we're close to the feature freeze, so I just want to say that
I'll be AFK starting 5:30am Friday New Zealand time (13:30 on the 8th
New York time), until Sunday ~4pm. . I really hope that if this needs
any tweaking it will be minor. I'll check my email before I leave on
Friday in the hope that if there's anything, then I can quickly fix it
up before I go, but time will be short.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2016-04-07.patchapplication/octet-stream; name=unique_joins_2016-04-07.patchDownload
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index ee0220a..59084c6 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -3923,10 +3923,13 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 	/*
 	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
 	 * Constructing queries representing SEMI and ANTI joins is hard, hence
-	 * not considered right now.
+	 * not considered right now. INNER_UNIQUE and LEFT_UNIQUE joins are ok
+	 * here, since they're merely an optimization their non-unique
+	 * counterparts.
 	 */
 	if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
-		jointype != JOIN_RIGHT && jointype != JOIN_FULL)
+		jointype != JOIN_RIGHT && jointype != JOIN_FULL &&
+		jointype != JOIN_INNER_UNIQUE && jointype != JOIN_LEFT_UNIQUE)
 		return false;
 
 	/*
@@ -4052,6 +4055,7 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 	switch (jointype)
 	{
 		case JOIN_INNER:
+		case JOIN_INNER_UNIQUE:
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
 										  list_copy(fpinfo_i->remote_conds));
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
@@ -4059,6 +4063,7 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 			break;
 
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
 										  list_copy(fpinfo_i->remote_conds));
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 713cd0e..e05925b 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1118,9 +1118,15 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					case JOIN_INNER:
 						jointype = "Inner";
 						break;
+					case JOIN_INNER_UNIQUE:
+						jointype = "Unique Inner";
+						break;
 					case JOIN_LEFT:
 						jointype = "Left";
 						break;
+					case JOIN_LEFT_UNIQUE:
+						jointype = "Unique Left";
+						break;
 					case JOIN_FULL:
 						jointype = "Full";
 						break;
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 369e666..196a075 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,13 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In an inner unique, left unique or semi join, we'll
+					 * consider returning the first match, but after that
+					 * we're done with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_INNER_UNIQUE ||
+						node->js.jointype == JOIN_LEFT_UNIQUE ||
+						node->js.jointype == JOIN_SEMI)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -499,9 +502,11 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	switch (node->join.jointype)
 	{
 		case JOIN_INNER:
+		case JOIN_INNER_UNIQUE:
 		case JOIN_SEMI:
 			break;
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_ANTI:
 			hjstate->hj_NullInnerTupleSlot =
 				ExecInitNullTupleSlot(estate,
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 6db09b8..aa3e144 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,13 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * In an inner unique, left unique or semi join, we'll
+					 * consider returning the first match, but after that
+					 * we're done with this outer tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_INNER_UNIQUE ||
+						node->js.jointype == JOIN_LEFT_UNIQUE ||
+						node->js.jointype == JOIN_SEMI)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1555,10 +1558,12 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	{
 		case JOIN_INNER:
 		case JOIN_SEMI:
+		case JOIN_INNER_UNIQUE:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_ANTI:
 			mergestate->mj_FillOuter = true;
 			mergestate->mj_FillInner = false;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 555fa09..0526170 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -182,6 +182,7 @@ ExecNestLoop(NestLoopState *node)
 
 			if (!node->nl_MatchedOuter &&
 				(node->js.jointype == JOIN_LEFT ||
+				 node->js.jointype == JOIN_LEFT_UNIQUE ||
 				 node->js.jointype == JOIN_ANTI))
 			{
 				/*
@@ -247,10 +248,13 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * In an inner unique, left unique or semi join, we'll consider
+			 * returning the first match, but after that we're done with this
+			 * outer tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.jointype == JOIN_INNER_UNIQUE ||
+				node->js.jointype == JOIN_LEFT_UNIQUE ||
+				node->js.jointype == JOIN_SEMI)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -355,9 +359,11 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	switch (node->join.jointype)
 	{
 		case JOIN_INNER:
+		case JOIN_INNER_UNIQUE:
 		case JOIN_SEMI:
 			break;
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_ANTI:
 			nlstate->nl_NullInnerTupleSlot =
 				ExecInitNullTupleSlot(estate,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index a21928b..a2a91e5 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2065,6 +2065,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_BITMAPSET_FIELD(syn_lefthand);
 	COPY_BITMAPSET_FIELD(syn_righthand);
 	COPY_SCALAR_FIELD(jointype);
+	COPY_SCALAR_FIELD(optimal_jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
 	COPY_SCALAR_FIELD(semi_can_btree);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3c6c567..3302a69 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -837,6 +837,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_BITMAPSET_FIELD(syn_lefthand);
 	COMPARE_BITMAPSET_FIELD(syn_righthand);
 	COMPARE_SCALAR_FIELD(jointype);
+	COMPARE_SCALAR_FIELD(optimal_jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index e39c374..3ab5095 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2266,6 +2266,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_BITMAPSET_FIELD(syn_lefthand);
 	WRITE_BITMAPSET_FIELD(syn_righthand);
 	WRITE_ENUM_FIELD(jointype, JoinType);
+	WRITE_ENUM_FIELD(optimal_jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
 	WRITE_BOOL_FIELD(semi_can_btree);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 70a4c27..e4d5cc9 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1893,8 +1893,9 @@ cost_group(Path *path, PlannerInfo *root,
  * estimate and getting a tight lower bound.  We choose to not examine the
  * join quals here, since that's by far the most expensive part of the
  * calculations.  The end result is that CPU-cost considerations must be
- * left for the second phase; and for SEMI/ANTI joins, we must also postpone
- * incorporation of the inner path's run cost.
+ * left for the second phase; and for INNER_UNIQUE, LEFT_UNIQUE, SEMI, and
+ * ANTI joins, we must also postpone incorporation of the inner path's run
+ * cost.
  *
  * 'workspace' is to be filled with startup_cost, total_cost, and perhaps
  *		other data to be used by final_cost_nestloop
@@ -1902,7 +1903,8 @@ cost_group(Path *path, PlannerInfo *root,
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
  * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'semifactors' contains valid data if jointype is INNER_UNIQUE, LEFT_UNIQUE,
+ * SEMI, or ANTI
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
@@ -1940,10 +1942,14 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_INNER_UNIQUE ||
+		jointype == JOIN_LEFT_UNIQUE ||
+		jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * INNER_UNIQUE, LEFT_UNIQUE, SEMI, or ANTI join: executor will stop
+		 * after first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -1977,7 +1983,8 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
  * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'semifactors' contains valid data if jointype is INNER_UNIQUE, LEFT_UNIQUE,
+ * SEMI, or ANTI
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
@@ -2017,10 +2024,14 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (path->jointype == JOIN_INNER_UNIQUE ||
+		path->jointype == JOIN_LEFT_UNIQUE ||
+		path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * INNER_UNIQUE, LEFT_UNIQUE, SEMI, or ANTI join: executor will stop
+		 * after first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2250,6 +2261,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerendsel = cache->leftendsel;
 		}
 		if (jointype == JOIN_LEFT ||
+			jointype == JOIN_LEFT_UNIQUE ||
 			jointype == JOIN_ANTI)
 		{
 			outerstartsel = 0.0;
@@ -2773,7 +2785,8 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
  * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'semifactors' contains valid data if path->jointype is INNER_UNIQUE,
+ * LEFT_UNIQUE, SEMI or ANTI
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
@@ -2896,13 +2909,17 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (path->jpath.jointype == JOIN_INNER_UNIQUE ||
+		path->jpath.jointype == JOIN_LEFT_UNIQUE ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * INNER_UNIQUE, LEFT_UNIQUE, SEMI or ANTI join: executor will stop
+		 * after first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
@@ -2937,10 +2954,10 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 			clamp_row_est(inner_path_rows / virtualbuckets) * 0.05;
 
 		/* Get # of tuples that will pass the basic join */
-		if (path->jpath.jointype == JOIN_SEMI)
-			hashjointuples = outer_matched_rows;
-		else
+		if (path->jpath.jointype == JOIN_ANTI)
 			hashjointuples = outer_path_rows - outer_matched_rows;
+		else
+			hashjointuples = outer_matched_rows;
 	}
 	else
 	{
@@ -3469,13 +3486,13 @@ get_restriction_qual_cost(PlannerInfo *root, RelOptInfo *baserel,
 
 /*
  * compute_semi_anti_join_factors
- *	  Estimate how much of the inner input a SEMI or ANTI join
- *	  can be expected to scan.
+ *	  Estimate how much of the inner input a INNER_UNIQUE, LEFT_UNIQUE, SEMI,
+ *	  ANTI join can be expected to scan.
  *
- * In a hash or nestloop SEMI/ANTI join, the executor will stop scanning
- * inner rows as soon as it finds a match to the current outer row.
- * We should therefore adjust some of the cost components for this effect.
- * This function computes some estimates needed for these adjustments.
+ * In a hash or nestloop INNER_UNIQUE/LEFT_UNIQUE/SEMI/ANTI join, the executor
+ * will stop scanning inner rows as soon as it finds a match to the current
+ * outer row. We should therefore adjust some of the cost components for this
+ * effect. This function computes some estimates needed for these adjustments.
  * These estimates will be the same regardless of the particular paths used
  * for the outer and inner relation, so we compute these once and then pass
  * them to all the join cost estimation functions.
@@ -3483,7 +3500,8 @@ get_restriction_qual_cost(PlannerInfo *root, RelOptInfo *baserel,
  * Input parameters:
  *	outerrel: outer relation under consideration
  *	innerrel: inner relation under consideration
- *	jointype: must be JOIN_SEMI or JOIN_ANTI
+ *	jointype: must be JOIN_INNER_UNIQUE, JOIN_LEFT_UNIQUE,JOIN_SEMI or
+ *	JOIN_ANTI
  *	sjinfo: SpecialJoinInfo relevant to this join
  *	restrictlist: join quals
  * Output parameters:
@@ -3506,7 +3524,10 @@ compute_semi_anti_join_factors(PlannerInfo *root,
 	ListCell   *l;
 
 	/* Should only be called in these cases */
-	Assert(jointype == JOIN_SEMI || jointype == JOIN_ANTI);
+	Assert(jointype == JOIN_INNER_UNIQUE ||
+		   jointype == JOIN_LEFT_UNIQUE ||
+		   jointype == JOIN_SEMI ||
+		   jointype == JOIN_ANTI);
 
 	/*
 	 * In an ANTI join, we must ignore clauses that are "pushed down", since
@@ -3530,7 +3551,8 @@ compute_semi_anti_join_factors(PlannerInfo *root,
 		joinquals = restrictlist;
 
 	/*
-	 * Get the JOIN_SEMI or JOIN_ANTI selectivity of the join clauses.
+	 * Get the JOIN_INNER_UNIQUE, JOIN_LEFT_UNIQUE, JOIN_SEMI or JOIN_ANTI
+	 * selectivity of the join clauses.
 	 */
 	jselec = clauselist_selectivity(root,
 									joinquals,
@@ -3967,8 +3989,13 @@ calc_joinrel_size_estimate(PlannerInfo *root,
 	 * pushed-down quals are applied after the outer join, so their
 	 * selectivity applies fully.
 	 *
-	 * For JOIN_SEMI and JOIN_ANTI, the selectivity is defined as the fraction
-	 * of LHS rows that have matches, and we apply that straightforwardly.
+	 * For JOIN_INNER_UNIQUE, JOIN_SEMI and JOIN_ANTI, the selectivity is
+	 * defined as the fraction of LHS rows that have matches, and we apply
+	 * that straightforwardly.
+	 *
+	 * For JOIN_SEMI_LEFT, the selectivity is defined as the fraction of the
+	 * LHS rows that have matches, although unlike JOIN_SEMI we must consider
+	 * NULL RHS rows, and take the higher estimate of the two.
 	 */
 	switch (jointype)
 	{
@@ -3990,9 +4017,16 @@ calc_joinrel_size_estimate(PlannerInfo *root,
 			nrows *= pselec;
 			break;
 		case JOIN_SEMI:
+		case JOIN_INNER_UNIQUE:
 			nrows = outer_rows * jselec;
 			/* pselec not used */
 			break;
+		case JOIN_LEFT_UNIQUE:
+			nrows = outer_rows * jselec;
+			if (nrows < outer_rows)
+				nrows = outer_rows;
+			nrows *= pselec;
+			break;
 		case JOIN_ANTI:
 			nrows = outer_rows * (1.0 - jselec);
 			nrows *= pselec;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 41b60d0..d08532d 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -50,6 +51,8 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
+static inline bool clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+						RelOptInfo *innerrel);
 
 
 /*
@@ -88,6 +91,13 @@ add_paths_to_joinrel(PlannerInfo *root,
 	bool		mergejoin_allowed = true;
 	ListCell   *lc;
 
+	/*
+	 * There may be a more optimal JoinType to use. Check for such cases
+	 * first.
+	 */
+	jointype = get_optimal_jointype(root, outerrel, innerrel, jointype, sjinfo,
+									restrictlist);
+
 	extra.restrictlist = restrictlist;
 	extra.mergeclause_list = NIL;
 	extra.sjinfo = sjinfo;
@@ -109,10 +119,14 @@ add_paths_to_joinrel(PlannerInfo *root,
 														  &mergejoin_allowed);
 
 	/*
-	 * If it's SEMI or ANTI join, compute correction factors for cost
-	 * estimation.  These will be the same for all paths.
+	 * If it's INNER_UNIQUE, LEFT_UNIQUE, SEMI or ANTI join, compute
+	 * correction factors for cost estimation.  These will be the same for all
+	 * paths.
 	 */
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (jointype == JOIN_INNER_UNIQUE ||
+		jointype == JOIN_LEFT_UNIQUE ||
+		jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI)
 		compute_semi_anti_join_factors(root, outerrel, innerrel,
 									   jointype, sjinfo, restrictlist,
 									   &extra.semifactors);
@@ -827,16 +841,18 @@ match_unsorted_outer(PlannerInfo *root,
 	ListCell   *lc1;
 
 	/*
-	 * Nestloop only supports inner, left, semi, and anti joins.  Also, if we
-	 * are doing a right or full mergejoin, we must use *all* the mergeclauses
-	 * as join clauses, else we will not have a valid plan.  (Although these
-	 * two flags are currently inverses, keep them separate for clarity and
-	 * possible future changes.)
+	 * Nestloop only supports inner, inner unique, left, left unique, semi,
+	 * and anti joins.  Also, if we are doing a right or full mergejoin, we
+	 * must use *all* the mergeclauses as join clauses, else we will not have
+	 * a valid plan. (Although these two flags are currently inverses, keep
+	 * them separate for clarity and possible future changes.)
 	 */
 	switch (jointype)
 	{
 		case JOIN_INNER:
+		case JOIN_INNER_UNIQUE:
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_SEMI:
 		case JOIN_ANTI:
 			nestjoinOK = true;
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 01d4fea..547d09d 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -490,10 +490,12 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 			/*
 			 * The proposed join could still be legal, but only if we're
 			 * allowed to associate it into the RHS of this SJ.  That means
-			 * this SJ must be a LEFT join (not SEMI or ANTI, and certainly
-			 * not FULL) and the proposed join must not overlap the LHS.
+			 * this SJ must be a LEFT or LEFT_UNIQUE join (not SEMI or ANTI,
+			 * and certainly not FULL) and the proposed join must not overlap
+			 * the LHS.
 			 */
-			if (sjinfo->jointype != JOIN_LEFT ||
+			if ((sjinfo->jointype != JOIN_LEFT &&
+				 sjinfo->jointype != JOIN_LEFT_UNIQUE) ||
 				bms_overlap(joinrelids, sjinfo->min_lefthand))
 				return false;	/* invalid join path */
 
@@ -508,8 +510,8 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 	}
 
 	/*
-	 * Fail if violated any SJ's RHS and didn't match to a LEFT SJ: the
-	 * proposed join can't associate into an SJ's RHS.
+	 * Fail if violated any SJ's RHS and didn't match to a LEFT or LEFT_UNIQUE
+	 * SJ: the proposed join can't associate into an SJ's RHS.
 	 *
 	 * Also, fail if the proposed join's predicate isn't strict; we're
 	 * essentially checking to see if we can apply outer-join identity 3, and
@@ -518,7 +520,8 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 	 */
 	if (must_be_leftjoin &&
 		(match_sjinfo == NULL ||
-		 match_sjinfo->jointype != JOIN_LEFT ||
+		 (match_sjinfo->jointype != JOIN_LEFT &&
+		  match_sjinfo->jointype != JOIN_LEFT_UNIQUE) ||
 		 !match_sjinfo->lhs_strict))
 		return false;			/* invalid join path */
 
@@ -698,6 +701,7 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 		sjinfo->syn_lefthand = rel1->relids;
 		sjinfo->syn_righthand = rel2->relids;
 		sjinfo->jointype = JOIN_INNER;
+		sjinfo->optimal_jointype = JOIN_INNER;
 		/* we don't bother trying to make the remaining fields valid */
 		sjinfo->lhs_strict = false;
 		sjinfo->delay_upper_joins = false;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index f7f5714..ba082d8 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -34,11 +34,43 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+						   SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
 
+/*
+ * optimize_outerjoin_types
+ *		Determine if the most optimal JoinType for each outer join, and update
+ *		SpecialJoinInfo optimal_jointype to that join type.
+ */
+void
+optimize_outerjoin_types(PlannerInfo *root, List *joinlist)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs. If we can prove
+		 * these to have a unique inner side, based on the join condition then
+		 * this can save the executor from having to attempt fruitless
+		 * searches for subsequent matching outer tuples.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT &&
+			sjinfo->optimal_jointype != JOIN_LEFT_UNIQUE &&
+			specialjoin_is_unique_join(root, sjinfo))
+			sjinfo->optimal_jointype = JOIN_LEFT_UNIQUE;
+	}
+}
 
 /*
  * remove_useless_joins
@@ -92,6 +124,12 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * We may now be able to optimize some joins which we could not
+		 * optimize before.
+		 */
+		optimize_outerjoin_types(root, joinlist);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -152,17 +190,15 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 {
 	int			innerrelid;
 	RelOptInfo *innerrel;
-	Query	   *subquery = NULL;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must have a unique inner side and must be a non-delaying join to a
+	 * single baserel, else we aren't going to be able to do anything with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (sjinfo->optimal_jointype != JOIN_LEFT_UNIQUE ||
 		sjinfo->delay_upper_joins)
 		return false;
 
@@ -171,38 +207,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	if (innerrel->reloptkind != RELOPT_BASEREL)
-		return false;
-
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/*
-		 * For a plain-relation innerrel, we only know how to prove uniqueness
-		 * by reference to unique indexes.  If there are no indexes then
-		 * there's certainly no unique indexes so there's no point in going
-		 * further.
-		 */
-		if (innerrel->indexlist == NIL)
-			return false;
-	}
-	else if (innerrel->rtekind == RTE_SUBQUERY)
-	{
-		subquery = root->simple_rte_array[innerrelid]->subquery;
-
-		/*
-		 * If the subquery has no qualities that support distinctness proofs
-		 * then there's no point in going further.
-		 */
-		if (!query_supports_distinctness(subquery))
-			return false;
-	}
-	else
-		return false;			/* unsupported rtekind */
+	/* Must be true as is_unique_join can only be set to true for base rels */
+	Assert(innerrel->reloptkind == RELOPT_BASEREL);
 
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
@@ -213,7 +219,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join, too, but in this case the join
+	 * would not have been optimized into a LEFT_UNIQUE join.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -254,6 +261,44 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most one inner row for any single outer row. False is returned if
+ *		there's insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	ListCell   *l;
+	List	   *clause_list = NIL;
+
+	/* if there's more than one relation involved, then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	if (innerrel->reloptkind != RELOPT_BASEREL)
+		return false;
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -275,10 +320,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we
+			 * can't say we're unique.
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -301,71 +344,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 		clause_list = lappend(clause_list, restrictinfo);
 	}
 
-	/*
-	 * relation_has_unique_index_for automatically adds any usable restriction
-	 * clauses for the innerrel, so we needn't do that here.  (XXX we are not
-	 * considering restriction clauses for subqueries; is that worth doing?)
-	 */
-
-	if (innerrel->rtekind == RTE_RELATION)
-	{
-		/* Now examine the indexes to see if we have a matching unique index */
-		if (relation_has_unique_index_for(root, innerrel, clause_list, NIL, NIL))
-			return true;
-	}
-	else	/* innerrel->rtekind == RTE_SUBQUERY */
-	{
-		List	   *colnos = NIL;
-		List	   *opids = NIL;
-
-		/*
-		 * Build the argument lists for query_is_distinct_for: a list of
-		 * output column numbers that the query needs to be distinct over, and
-		 * a list of equality operators that the output columns need to be
-		 * distinct according to.
-		 */
-		foreach(l, clause_list)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-			Oid			op;
-			Var		   *var;
-
-			/*
-			 * Get the equality operator we need uniqueness according to.
-			 * (This might be a cross-type operator and thus not exactly the
-			 * same operator the subquery would consider; that's all right
-			 * since query_is_distinct_for can resolve such cases.)  The
-			 * mergejoinability test above should have selected only OpExprs.
-			 */
-			Assert(IsA(rinfo->clause, OpExpr));
-			op = ((OpExpr *) rinfo->clause)->opno;
-
-			/* clause_sides_match_join identified the inner side for us */
-			if (rinfo->outer_is_left)
-				var = (Var *) get_rightop(rinfo->clause);
-			else
-				var = (Var *) get_leftop(rinfo->clause);
-
-			/*
-			 * If inner side isn't a Var referencing a subquery output column,
-			 * this clause doesn't help us.
-			 */
-			if (!var || !IsA(var, Var) ||
-				var->varno != innerrelid || var->varlevelsup != 0)
-				continue;
-
-			colnos = lappend_int(colnos, var->varattno);
-			opids = lappend_oid(opids, op);
-		}
-
-		if (query_is_distinct_for(subquery, colnos, opids))
-			return true;
-	}
+	if (rel_is_distinct_for(root, innerrel, clause_list))
+		return true;
 
-	/*
-	 * Some day it would be nice to check for other methods of establishing
-	 * distinctness.
-	 */
 	return false;
 }
 
@@ -560,6 +541,127 @@ remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved)
 	return result;
 }
 
+/*
+ * rel_is_distinct_for
+ *		Returns True if rel can be proved to be distinct over clause_list
+ *
+ * Note: We expect clause_list to be already processed to check if the
+ * RestrictInfos are in the form "outerrel_expr op innerrel_expr" or
+ * "innerrel_expr op outerrel_expr".
+ *
+ * Note: this method may add items to clause_list. If callers care about the
+ * list contents, then they should either make a copy of the list, or trim it
+ * back to it's original length, after calling this function.
+ */
+bool
+rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
+{
+	int			relid = rel->relid;
+
+	/*
+	 * relation_has_unique_index_for automatically adds any usable restriction
+	 * clauses for the rel, so we needn't do that here.  (XXX we are not
+	 * considering restriction clauses for subqueries; is that worth doing?)
+	 */
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/* Now examine the indexes to see if we have a matching unique index */
+		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		List	   *colnos = NIL;
+		List	   *opids = NIL;
+		ListCell   *l;
+		Query	   *subquery = root->simple_rte_array[relid]->subquery;
+
+		/*
+		 * Build the argument lists for query_is_distinct_for: a list of
+		 * output column numbers that the query needs to be distinct over, and
+		 * a list of equality operators that the output columns need to be
+		 * distinct according to.
+		 */
+		foreach(l, clause_list)
+		{
+			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
+			Oid			op;
+			Var		   *var;
+
+			if (!IsA(rinfo->clause, OpExpr))
+				continue;
+
+			/*
+			 * Get the equality operator we need uniqueness according to.
+			 * (This might be a cross-type operator and thus not exactly the
+			 * same operator the subquery would consider; that's all right
+			 * since query_is_distinct_for can resolve such cases.)  The
+			 * mergejoinability test above should have selected only OpExprs.
+			 */
+			op = ((OpExpr *) rinfo->clause)->opno;
+
+			/* clause_sides_match_join identified the inner side for us */
+			if (rinfo->outer_is_left)
+				var = (Var *) get_rightop(rinfo->clause);
+			else
+				var = (Var *) get_leftop(rinfo->clause);
+
+			/*
+			 * If inner side isn't a Var referencing a subquery output column,
+			 * this clause doesn't help us.
+			 */
+			if (!var || !IsA(var, Var) ||
+				var->varno != relid || var->varlevelsup != 0)
+				continue;
+
+			colnos = lappend_int(colnos, var->varattno);
+			opids = lappend_oid(opids, op);
+		}
+
+		if (query_is_distinct_for(subquery, colnos, opids))
+			return true;
+	}
+	return false;				/* can't prove rel to be distinct over
+								 * clause_list */
+}
+
+/*
+ * rel_supports_distinctness
+ *		Returns true if rel has some properties which can prove the relation
+ *		to be unique over some set of columns.
+ *
+ * This is effectively a pre-checking function for rel_is_distinct_for().
+ * It must return TRUE if rel_is_distinct_for() could possibly return TRUE
+ */
+bool
+rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
+{
+	if (rel->rtekind == RTE_RELATION)
+	{
+		/*
+		 * For a plain-relation, we only know how to prove uniqueness by
+		 * reference to unique indexes.  If there are no indexes then there's
+		 * certainly no unique indexes so there's nothing to prove uniqueness
+		 * on the relation.
+		 */
+		if (rel->indexlist != NIL)
+			return true;
+	}
+	else if (rel->rtekind == RTE_SUBQUERY)
+	{
+		Query	   *subquery = root->simple_rte_array[rel->relid]->subquery;
+
+		/* Check if the subquery has any qualities that support distinctness */
+		if (query_supports_distinctness(subquery))
+			return true;
+	}
+
+	/*
+	 * Some day it would be nice to check for other methods of establishing
+	 * distinctness.
+	 */
+	return false;
+}
 
 /*
  * query_supports_distinctness - could the query possibly be proven distinct
@@ -767,3 +869,163 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	bool		is_unique;
+	int			org_len;
+	ListCell   *lc;
+
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Remember the number of items that were in the restrictlist as the call
+	 * to relation_has_unique_index_for may add more items which we'll need to
+	 * remove later.
+	 */
+	org_len = list_length(restrictlist);
+
+	/*
+	 * rel_is_distinct_for requires restrict infos to have the correct clause
+	 * direction info
+	 */
+	foreach(lc, restrictlist)
+	{
+		clause_sides_match_join((RestrictInfo *) lfirst(lc), outerrel->relids,
+								innerrel->relids);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	is_unique = rel_is_distinct_for(root, innerrel, restrictlist);
+
+	/* Remove any list items added by rel_is_distinct_for */
+	list_truncate(restrictlist, org_len);
+
+	return is_unique;
+}
+
+/*
+ * get_optimal_jointype
+ *	  We may be able to optimize some joins by converting the JoinType to one
+ *	  which the executor is able to run more efficiently. Here we look for
+ *	  such cases and if we find a better choice, then we'll return it,
+ *	  otherwise we'll return the original JoinType.
+ */
+JoinType
+get_optimal_jointype(PlannerInfo *root,
+					 RelOptInfo *outerrel,
+					 RelOptInfo *innerrel,
+					 JoinType jointype,
+					 SpecialJoinInfo *sjinfo,
+					 List *restrictlist)
+{
+	int			innerrelid;
+
+	/* left joins were already optimized in optimize_outerjoin_types() */
+	if (jointype == JOIN_LEFT)
+		return sjinfo->optimal_jointype;
+
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return jointype;
+
+	/*
+	 * Any INNER JOINs which can be proven to return at most one inner tuple
+	 * for each outer tuple can be converted in to a JOIN_SEMI.
+	 */
+	if (jointype == JOIN_INNER)
+	{
+		MemoryContext old_context;
+		ListCell   *lc;
+
+		/* can't optimize jointype with an empty restrictlist */
+		if (restrictlist == NIL)
+			return jointype;
+
+		/*
+		 * First let's query the unique and non-unique caches to see if we've
+		 * managed to prove that innerrel is unique for some subset of this
+		 * outerrel. We don't need an exact match, as if we have any extra
+		 * outerrels than were previously cached, then they can't make the
+		 * innerrel any less unique.
+		 */
+		foreach(lc, root->unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(unique_rels, outerrel->relids))
+			{
+				/* ensure is_innerrel_unique_for() agrees */
+				Assert(is_innerrel_unique_for(root, outerrel, innerrel,
+											  restrictlist));
+
+				return JOIN_INNER_UNIQUE;		/* Success! */
+			}
+		}
+
+		/*
+		 * We may have previously determined that this outerrel, or some
+		 * superset thereof, cannot prove this innerrel to be unique.
+		 */
+		foreach(lc, root->non_unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(outerrel->relids, unique_rels))
+			{
+				/* ensure is_innerrel_unique_for() agrees */
+				Assert(!is_innerrel_unique_for(root, outerrel, innerrel,
+											   restrictlist));
+
+				return jointype;
+			}
+		}
+
+		/*
+		 * We may be getting called from the geqo and might not be working in
+		 * the standard planner's memory context, so let's ensure we are.
+		 */
+		old_context = MemoryContextSwitchTo(root->planner_cxt);
+
+		if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+		{
+			/*
+			 * XXX Should we attempt to get the minimum set of outerrels which
+			 * proved this innerrel to be unique? If we did this then we might
+			 * be able to make a few more cases unique that we otherwise
+			 * couldn't. However, the standard join search always start with
+			 * fewer rels anyway, so this may not matter, although perhaps we
+			 * should be more aggressive to make this work better with the
+			 * geqo?
+			 */
+
+			/* cache the result for next time */
+			root->unique_rels[innerrelid] =
+				lappend(root->unique_rels[innerrelid], outerrel->relids);
+
+			jointype = JOIN_INNER_UNIQUE;		/* Success! */
+		}
+		else
+		{
+			/*
+			 * None of outerrel helped prove innerrel unique, so we can safely
+			 * reject this rel, or a subset of this rel in future checks.
+			 */
+			root->non_unique_rels[innerrelid] =
+				lappend(root->non_unique_rels[innerrelid], outerrel->relids);
+		}
+
+		MemoryContextSwitchTo(old_context);
+	}
+	return jointype;
+}
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 9999eea..a615680 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1128,6 +1128,8 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->syn_lefthand = left_rels;
 	sjinfo->syn_righthand = right_rels;
 	sjinfo->jointype = jointype;
+	/* this may be changed later */
+	sjinfo->optimal_jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index edd95d8..d027000 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
@@ -185,6 +188,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* Determine if there's a better JoinType for each outer join */
+	optimize_outerjoin_types(root, joinlist);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 5537c14..95c3a2f 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -1617,6 +1617,7 @@ set_join_references(PlannerInfo *root, Join *join, int rtoffset)
 	switch (join->jointype)
 	{
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_SEMI:
 		case JOIN_ANTI:
 			inner_itlist->has_non_vars = false;
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 802eab3..c11769e 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -81,6 +81,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 751de4b..66eb670 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -978,11 +978,11 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		/*
 		 * Make the left-side RTEs available for LATERAL access within the
 		 * right side, by temporarily adding them to the pstate's namespace
-		 * list.  Per SQL:2008, if the join type is not INNER or LEFT then the
-		 * left-side names must still be exposed, but it's an error to
-		 * reference them.  (Stupid design, but that's what it says.)  Hence,
-		 * we always push them into the namespace, but mark them as not
-		 * lateral_ok if the jointype is wrong.
+		 * list.  Per SQL:2008, if the join type is not INNER, INNER_UNIQUE,
+		 * LEFT or LEFT_UNIQUE then the left-side names must still be exposed,
+		 * but it's an error to reference them.  (Stupid design, but that's
+		 * what it says.)  Hence, we always push them into the namespace, but
+		 * mark them as not lateral_ok if the jointype is wrong.
 		 *
 		 * Notice that we don't require the merged namespace list to be
 		 * conflict-free.  See the comments for scanNameSpaceForRefname().
@@ -990,7 +990,10 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		 * NB: this coding relies on the fact that list_concat is not
 		 * destructive to its second argument.
 		 */
-		lateral_ok = (j->jointype == JOIN_INNER || j->jointype == JOIN_LEFT);
+		lateral_ok = (j->jointype == JOIN_INNER ||
+					  j->jointype == JOIN_INNER_UNIQUE ||
+					  j->jointype == JOIN_LEFT ||
+					  j->jointype == JOIN_LEFT_UNIQUE);
 		setNamespaceLateralState(l_namespace, true, lateral_ok);
 
 		sv_namespace_length = list_length(pstate->p_namespace);
diff --git a/src/backend/utils/adt/network_selfuncs.c b/src/backend/utils/adt/network_selfuncs.c
index 2e39687..f14a3cb 100644
--- a/src/backend/utils/adt/network_selfuncs.c
+++ b/src/backend/utils/adt/network_selfuncs.c
@@ -215,7 +215,9 @@ networkjoinsel(PG_FUNCTION_ARGS)
 	switch (sjinfo->jointype)
 	{
 		case JOIN_INNER:
+		case JOIN_INNER_UNIQUE:
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_FULL:
 
 			/*
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index cc2a9a1..346952e 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -2202,6 +2202,7 @@ eqjoinsel(PG_FUNCTION_ARGS)
 	{
 		case JOIN_INNER:
 		case JOIN_LEFT:
+		case JOIN_LEFT_UNIQUE:
 		case JOIN_FULL:
 			selec = eqjoinsel_inner(operator, &vardata1, &vardata2);
 			break;
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 3f22bdb..556048a 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -639,6 +639,16 @@ typedef enum JoinType
 	JOIN_ANTI,					/* 1 copy of each LHS row that has no match */
 
 	/*
+	 * The following join types are a variants of JOIN_INNER and JOIN_LEFT for
+	 * when the inner side of the join is known to be unique. This serves
+	 * solely as an optimization to allow the executor to skip looking for
+	 * another matching tuple in the inner side, when it's known that another
+	 * cannot exist.
+	 */
+	JOIN_INNER_UNIQUE,
+	JOIN_LEFT_UNIQUE,
+
+	/*
 	 * These codes are used internally in the planner, but are not supported
 	 * by the executor (nor, indeed, by most of the planner).
 	 */
@@ -667,6 +677,7 @@ typedef enum JoinType
 #define IS_OUTER_JOIN(jointype) \
 	(((1 << (jointype)) & \
 	  ((1 << JOIN_LEFT) | \
+	   (1 << JOIN_LEFT_UNIQUE) | \
 	   (1 << JOIN_FULL) | \
 	   (1 << JOIN_RIGHT) | \
 	   (1 << JOIN_ANTI))) != 0)
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 5264d3c..6ca6da2 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -221,6 +221,18 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to optimize joins to try to prove
+	 * their inner side to be unique based on the join condition. This is a
+	 * rather expensive thing to do as it requires checking each relations
+	 * unique indexes to see if the relation can, at most, return one tuple
+	 * for each outer tuple. We use this cache during the join search to
+	 * record lists of the sets of relations which both prove, and disprove
+	 * the uniqueness properties for the relid indexed by these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1728,6 +1740,8 @@ typedef struct SpecialJoinInfo
 	Relids		syn_lefthand;	/* base relids syntactically within LHS */
 	Relids		syn_righthand;	/* base relids syntactically within RHS */
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
+	JoinType	optimal_jointype;		/* as above but may also be
+										 * INNER_UNIQUE or LEFT_UNIQUE */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index acc827d..13b212f 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -236,6 +236,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index da9c640..a3decb5 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -99,9 +99,19 @@ extern RestrictInfo *build_implied_join_equality(Oid opno,
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void optimize_outerjoin_types(PlannerInfo *root, List *joinlist);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
+extern bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
+					List *clause_list);
+extern bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
+extern JoinType get_optimal_jointype(PlannerInfo *root,
+					 RelOptInfo *outerrel,
+					 RelOptInfo *innerrel,
+					 JoinType jointype,
+					 SpecialJoinInfo *sjinfo,
+					 List *restrictlist);
 
 /*
  * prototypes for plan/setrefs.c
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 3ff6691..691fa11 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -880,29 +880,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Unique Inner Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Unique Inner Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 0391b8e..f446284 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -186,7 +186,7 @@ explain (costs off)
   select * from ec1, ec2 where ff = x1 and x1 = '42'::int8alias2;
                QUERY PLAN                
 -----------------------------------------
- Nested Loop
+ Nested Loop Unique Inner Join
    ->  Seq Scan on ec2
          Filter: (x1 = '42'::int8alias2)
    ->  Index Scan using ec1_pkey on ec1
@@ -310,7 +310,7 @@ explain (costs off)
          ->  Index Scan using ec1_expr3 on ec1 ec1_5
          ->  Index Scan using ec1_expr4 on ec1 ec1_6
    ->  Materialize
-         ->  Merge Join
+         ->  Merge Unique Inner Join
                Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
                ->  Merge Append
                      Sort Key: (((ec1_1.ff + 2) + 1))
@@ -365,7 +365,7 @@ explain (costs off)
   where ss1.x = ec1.f1 and ec1.ff = 42::int8;
                      QUERY PLAN                      
 -----------------------------------------------------
- Merge Join
+ Merge Unique Inner Join
    Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
    ->  Merge Append
          Sort Key: (((ec1_1.ff + 2) + 1))
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index cafbc5e..7d0aaa7 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2756,8 +2756,8 @@ from nt3 as nt3
 where nt3.id = 1 and ss2.b3;
                   QUERY PLAN                   
 -----------------------------------------------
- Nested Loop
-   ->  Nested Loop
+ Nested Loop Unique Inner Join
+   ->  Nested Loop Unique Inner Join
          ->  Index Scan using nt3_pkey on nt3
                Index Cond: (id = 1)
          ->  Index Scan using nt2_pkey on nt2
@@ -4035,7 +4035,7 @@ explain (costs off)
     on (p.k = ss.k);
            QUERY PLAN            
 ---------------------------------
- Hash Left Join
+ Hash Unique Left Join
    Hash Cond: (p.k = c.k)
    ->  Seq Scan on parent p
    ->  Hash
@@ -5260,3 +5260,238 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to change joins into their appropriate semi join
+-- type
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Unique Inner Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Unique Inner Join
+   Output: j1.id, j3.id
+   Hash Cond: (j3.id = j1.id)
+   ->  Seq Scan on public.j3
+         Output: j3.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Unique Left Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Unique Left Join
+   Output: j1.id, j2.id
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(9 rows)
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(8 rows)
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Unique Inner Join
+   Output: j1.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop Unique Inner Join
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(14 rows)
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop Unique Inner Join
+   Output: j1.id, j3.id
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of join conversions
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(7 rows)
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop Unique Inner Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 067aa8d..9fdc695 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -276,7 +276,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle);
 EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
                      QUERY PLAN                     
 ----------------------------------------------------
- Nested Loop
+ Nested Loop Unique Inner Join
    ->  Subquery Scan on document
          Filter: f_leak(document.dtitle)
          ->  Seq Scan on document document_1
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 7f57526..2651ff8 100644
--- a/src/test/regress/expected/select_views.out
+++ b/src/test/regress/expected/select_views.out
@@ -1411,7 +1411,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Unique Inner Join
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1432,7 +1432,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Unique Inner Join
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1466,7 +1466,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Unique Inner Join
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1497,7 +1497,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Unique Inner Join
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/expected/select_views_1.out b/src/test/regress/expected/select_views_1.out
index 5275ef0..81795d8 100644
--- a/src/test/regress/expected/select_views_1.out
+++ b/src/test/regress/expected/select_views_1.out
@@ -1411,7 +1411,7 @@ NOTICE:  f_leak => 9801-2345-6789-0123
 EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                        QUERY PLAN                        
 ---------------------------------------------------------
- Hash Join
+ Hash Unique Inner Join
    Hash Cond: (r.cid = l.cid)
    ->  Seq Scan on credit_card r
          Filter: f_leak(cnum)
@@ -1432,7 +1432,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum);
 ---------------------------------------------------------------
  Subquery Scan on my_credit_card_secure
    Filter: f_leak(my_credit_card_secure.cnum)
-   ->  Hash Join
+   ->  Hash Unique Inner Join
          Hash Cond: (r.cid = l.cid)
          ->  Seq Scan on credit_card r
          ->  Hash
@@ -1466,7 +1466,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
    ->  Materialize
          ->  Subquery Scan on l
                Filter: f_leak(l.cnum)
-               ->  Hash Join
+               ->  Hash Unique Inner Join
                      Hash Cond: (r_1.cid = l_1.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
@@ -1497,7 +1497,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure
          ->  Seq Scan on credit_usage r
                Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
          ->  Materialize
-               ->  Hash Join
+               ->  Hash Unique Inner Join
                      Hash Cond: (r_1.cid = l.cid)
                      ->  Seq Scan on credit_card r_1
                      ->  Hash
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 3430f91..38cc39c 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1696,3 +1696,96 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to change joins into their appropriate semi join
+-- type
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of join conversions
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#75Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#74)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

I've attached an updated patch which introduces JOIN_INNER_UNIQUE and
JOIN_LEFT_UNIQUE. So unique inner joins no longer borrow JOIN_SEMI.

OK.

In EXPLAIN, I named these new join types "Unique Inner" and "Unique
Left".

Hm. I'm back to being unhappy about the amount of churn introduced
into the regression test outputs by this patch. I wonder whether we
could get away with only mentioning the "unique" aspect in VERBOSE
mode.

I'm also a bit suspicious of the fact that some of the plans in
aggregates.out changed from merge to hash joins; with basically
no stats at hand in those tests, that seems dubious. A quick look
at what the patch touched in costsize.c suggests that this might
be because you've effectively allowed cost_hashjoin to give a cost
discount for inner unique, but provided no similar intelligence
in cost_mergejoin.

I know we're close to the feature freeze, so I just want to say that
I'll be AFK starting 5:30am Friday New Zealand time (13:30 on the 8th
New York time), until Sunday ~4pm.

Understood. I don't know if we'll get this in or not, but I'll work
on it.

regards, tom lane

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

#76Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#74)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

[ unique_joins_2016-04-07.patch ]

Just had a thought about this, which should have crystallized a long
time ago perhaps. Where I'd originally imagined you were going with
this idea is to do what the thread title actually says, and check for
joins in which the *outer* side is unique. I can't see that that's
of any value for nestloop or hash joins, but for merge joins, knowing
that the outer side is unique could be extremely valuable because
we could skip doing mark/restore backups on the inner side, hugely
reducing the cost when the inner side has many duplicates. Now I'm
not asking for the initially-committed version of the patch to do that,
but I think we need to design it to be readily extensible to do so.

The problem with this is that it blows the current factorization around
add_paths_to_joinrel out of the water. What we'd want is for the caller
(make_join_rel) to determine uniqueness on both sides, and pass that info
down to each of its two calls of add_paths_to_joinrel; otherwise we have
to do double the work because each run of add_paths_to_joinrel will have
to make those same two determinations.

This probably also means that encoding the uniqueness into JoinType is
a lost cause. Expanding JOIN_INNER into four variants depending on
whether either or both sides are known unique, and ditto for JOIN_LEFT,
doesn't seem attractive at all. I suspect we want to go back to your
original design with a separate bool flag (well, two bools now, but
anyway separate from JoinType). Or maybe the variant JoinTypes still
are better, since they'd fit into switch() tests more naturally, but
it's a lot more debatable as to whether that notation is a win.

I apologize for leading you down the wrong path on the notational aspect,
but sometimes the right answer isn't clear till you've tried all the
possibilities.

Anyway, while refactoring the make_join_rel/add_paths_to_joinrel division
of labor wouldn't be such a big deal in itself, I don't want to commit a
change to JoinType only to undo it later; that would be too much churn.
So I think we need to resolve this question before we can move forward.
I don't know if you have time to look at this now --- my clock says it's
already Friday morning in New Zealand.

regards, tom lane

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

#77Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Tom Lane (#76)
Re: Performance improvement for joins where outer side is unique

Tom Lane wrote:

Anyway, while refactoring the make_join_rel/add_paths_to_joinrel division
of labor wouldn't be such a big deal in itself, I don't want to commit a
change to JoinType only to undo it later; that would be too much churn.
So I think we need to resolve this question before we can move forward.
I don't know if you have time to look at this now --- my clock says it's
already Friday morning in New Zealand.

FWIW the feature freeze rules state that it is allowed for a committer
to request an extension to the feature freeze date for individual
patches:
/messages/by-id/CA+TgmoY56w5FOzeEo+i48qehL+BsVTwy-Q1M0xjUhUCwgGW7-Q@mail.gmail.com
It seems to me that the restrictions laid out there are well met for
this patch, if you only need a couple of additional days for this patch
to get in.

--
�lvaro Herrera 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

#78Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alvaro Herrera (#77)
Re: Performance improvement for joins where outer side is unique

Alvaro Herrera <alvherre@2ndquadrant.com> writes:

Tom Lane wrote:

I don't know if you have time to look at this now --- my clock says it's
already Friday morning in New Zealand.

FWIW the feature freeze rules state that it is allowed for a committer
to request an extension to the feature freeze date for individual
patches:
/messages/by-id/CA+TgmoY56w5FOzeEo+i48qehL+BsVTwy-Q1M0xjUhUCwgGW7-Q@mail.gmail.com
It seems to me that the restrictions laid out there are well met for
this patch, if you only need a couple of additional days for this patch
to get in.

Hmm ... the changes I'm thinking about here are certainly pretty
mechanical, if tedious. The main question mark I have hanging over
this patch is whether the planning-time penalty is too large --- but
that's something that can be tested with the patch as it stands.
Let me go investigate that a bit before requesting an extension.

regards, tom lane

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

#79Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#78)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

I wrote:

Alvaro Herrera <alvherre@2ndquadrant.com> writes:

FWIW the feature freeze rules state that it is allowed for a committer
to request an extension to the feature freeze date for individual
patches:
/messages/by-id/CA+TgmoY56w5FOzeEo+i48qehL+BsVTwy-Q1M0xjUhUCwgGW7-Q@mail.gmail.com
It seems to me that the restrictions laid out there are well met for
this patch, if you only need a couple of additional days for this patch
to get in.

Hmm ... the changes I'm thinking about here are certainly pretty
mechanical, if tedious. The main question mark I have hanging over
this patch is whether the planning-time penalty is too large --- but
that's something that can be tested with the patch as it stands.
Let me go investigate that a bit before requesting an extension.

I did some performance testing on the attached somewhat-cleaned-up patch,
and convinced myself that the planning time penalty is fairly minimal:
on the order of a couple percent in simple one-join queries, and less
than that in very large queries. Oddly, it seems that the result cacheing
done in get_optimal_jointype() is only barely worth the trouble in typical
cases; though if you get a query large enough to require GEQO, it's a win
because successive GEQO attempts can re-use cache entries made by earlier
attempts. (This may indicate something wrong with my testing procedure?
Seems like diking out the cache should have made more difference.)

I did find by measurement that the negative-cache-entry code produces
exactly zero hits unless you're in GEQO mode, which is not really
surprising given the order in which the join search occurs. So in the
attached patch I made the code not bother with making negative cache
entries unless using GEQO, to hopefully save a few nanoseconds.

I rebased over f338dd758, did a little bit of code cleanup and fixed some
bugs in the uniqueness detection logic, but have not reviewed the rest of
the patch since it's likely all gonna change if we reconsider the JoinType
representation.

Anyway, I think it would be reasonable to give this patch a few more
days in view of David's being away through the weekend. But the RMT
has final say on that.

regards, tom lane

Attachments:

unique_joins_2016-04-07-2.patchtext/x-diff; charset=us-ascii; name=unique_joins_2016-04-07-2.patchDownload
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 50f1261..f9df294 100644
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
*************** EXPLAIN (VERBOSE, COSTS false)
*** 386,392 ****
  ---------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2."C 1"
!    ->  Merge Join
           Output: t1.c1, t2."C 1"
           Merge Cond: (t1.c1 = t2."C 1")
           ->  Foreign Scan on public.ft2 t1
--- 386,392 ----
  ---------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2."C 1"
!    ->  Merge Unique Inner Join
           Output: t1.c1, t2."C 1"
           Merge Cond: (t1.c1 = t2."C 1")
           ->  Foreign Scan on public.ft2 t1
*************** EXPLAIN (VERBOSE, COSTS false)
*** 419,425 ****
  ---------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2."C 1"
!    ->  Merge Left Join
           Output: t1.c1, t2."C 1"
           Merge Cond: (t1.c1 = t2."C 1")
           ->  Foreign Scan on public.ft2 t1
--- 419,425 ----
  ---------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2."C 1"
!    ->  Merge Unique Left Join
           Output: t1.c1, t2."C 1"
           Merge Cond: (t1.c1 = t2."C 1")
           ->  Foreign Scan on public.ft2 t1
*************** explain (verbose, costs off) select * fr
*** 2520,2526 ****
    where f.f3 = l.f3 COLLATE "POSIX" and l.f1 = 'foo';
                           QUERY PLAN                          
  -------------------------------------------------------------
!  Hash Join
     Output: f.f1, f.f2, f.f3, l.f1, l.f2, l.f3
     Hash Cond: ((f.f3)::text = (l.f3)::text)
     ->  Foreign Scan on public.ft3 f
--- 2520,2526 ----
    where f.f3 = l.f3 COLLATE "POSIX" and l.f1 = 'foo';
                           QUERY PLAN                          
  -------------------------------------------------------------
!  Hash Unique Inner Join
     Output: f.f1, f.f2, f.f3, l.f1, l.f2, l.f3
     Hash Cond: ((f.f3)::text = (l.f3)::text)
     ->  Foreign Scan on public.ft3 f
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index ee0220a..59084c6 100644
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
*************** foreign_join_ok(PlannerInfo *root, RelOp
*** 3923,3932 ****
  	/*
  	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
  	 * Constructing queries representing SEMI and ANTI joins is hard, hence
! 	 * not considered right now.
  	 */
  	if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
! 		jointype != JOIN_RIGHT && jointype != JOIN_FULL)
  		return false;
  
  	/*
--- 3923,3935 ----
  	/*
  	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
  	 * Constructing queries representing SEMI and ANTI joins is hard, hence
! 	 * not considered right now. INNER_UNIQUE and LEFT_UNIQUE joins are ok
! 	 * here, since they're merely an optimization their non-unique
! 	 * counterparts.
  	 */
  	if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
! 		jointype != JOIN_RIGHT && jointype != JOIN_FULL &&
! 		jointype != JOIN_INNER_UNIQUE && jointype != JOIN_LEFT_UNIQUE)
  		return false;
  
  	/*
*************** foreign_join_ok(PlannerInfo *root, RelOp
*** 4052,4057 ****
--- 4055,4061 ----
  	switch (jointype)
  	{
  		case JOIN_INNER:
+ 		case JOIN_INNER_UNIQUE:
  			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
*************** foreign_join_ok(PlannerInfo *root, RelOp
*** 4059,4064 ****
--- 4063,4069 ----
  			break;
  
  		case JOIN_LEFT:
+ 		case JOIN_LEFT_UNIQUE:
  			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
  			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 713cd0e..e05925b 100644
*** a/src/backend/commands/explain.c
--- b/src/backend/commands/explain.c
*************** ExplainNode(PlanState *planstate, List *
*** 1118,1126 ****
--- 1118,1132 ----
  					case JOIN_INNER:
  						jointype = "Inner";
  						break;
+ 					case JOIN_INNER_UNIQUE:
+ 						jointype = "Unique Inner";
+ 						break;
  					case JOIN_LEFT:
  						jointype = "Left";
  						break;
+ 					case JOIN_LEFT_UNIQUE:
+ 						jointype = "Unique Left";
+ 						break;
  					case JOIN_FULL:
  						jointype = "Full";
  						break;
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 369e666..196a075 100644
*** a/src/backend/executor/nodeHashjoin.c
--- b/src/backend/executor/nodeHashjoin.c
*************** ExecHashJoin(HashJoinState *node)
*** 306,315 ****
  					}
  
  					/*
! 					 * In a semijoin, we'll consider returning the first
! 					 * match, but after that we're done with this outer tuple.
  					 */
! 					if (node->js.jointype == JOIN_SEMI)
  						node->hj_JoinState = HJ_NEED_NEW_OUTER;
  
  					if (otherqual == NIL ||
--- 306,318 ----
  					}
  
  					/*
! 					 * In an inner unique, left unique or semi join, we'll
! 					 * consider returning the first match, but after that
! 					 * we're done with this outer tuple.
  					 */
! 					if (node->js.jointype == JOIN_INNER_UNIQUE ||
! 						node->js.jointype == JOIN_LEFT_UNIQUE ||
! 						node->js.jointype == JOIN_SEMI)
  						node->hj_JoinState = HJ_NEED_NEW_OUTER;
  
  					if (otherqual == NIL ||
*************** ExecInitHashJoin(HashJoin *node, EState 
*** 499,507 ****
--- 502,512 ----
  	switch (node->join.jointype)
  	{
  		case JOIN_INNER:
+ 		case JOIN_INNER_UNIQUE:
  		case JOIN_SEMI:
  			break;
  		case JOIN_LEFT:
+ 		case JOIN_LEFT_UNIQUE:
  		case JOIN_ANTI:
  			hjstate->hj_NullInnerTupleSlot =
  				ExecInitNullTupleSlot(estate,
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 6db09b8..aa3e144 100644
*** a/src/backend/executor/nodeMergejoin.c
--- b/src/backend/executor/nodeMergejoin.c
*************** ExecMergeJoin(MergeJoinState *node)
*** 840,849 ****
  					}
  
  					/*
! 					 * In a semijoin, we'll consider returning the first
! 					 * match, but after that we're done with this outer tuple.
  					 */
! 					if (node->js.jointype == JOIN_SEMI)
  						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
  
  					qualResult = (otherqual == NIL ||
--- 840,852 ----
  					}
  
  					/*
! 					 * In an inner unique, left unique or semi join, we'll
! 					 * consider returning the first match, but after that
! 					 * we're done with this outer tuple.
  					 */
! 					if (node->js.jointype == JOIN_INNER_UNIQUE ||
! 						node->js.jointype == JOIN_LEFT_UNIQUE ||
! 						node->js.jointype == JOIN_SEMI)
  						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
  
  					qualResult = (otherqual == NIL ||
*************** ExecInitMergeJoin(MergeJoin *node, EStat
*** 1555,1564 ****
--- 1558,1569 ----
  	{
  		case JOIN_INNER:
  		case JOIN_SEMI:
+ 		case JOIN_INNER_UNIQUE:
  			mergestate->mj_FillOuter = false;
  			mergestate->mj_FillInner = false;
  			break;
  		case JOIN_LEFT:
+ 		case JOIN_LEFT_UNIQUE:
  		case JOIN_ANTI:
  			mergestate->mj_FillOuter = true;
  			mergestate->mj_FillInner = false;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 555fa09..0526170 100644
*** a/src/backend/executor/nodeNestloop.c
--- b/src/backend/executor/nodeNestloop.c
*************** ExecNestLoop(NestLoopState *node)
*** 182,187 ****
--- 182,188 ----
  
  			if (!node->nl_MatchedOuter &&
  				(node->js.jointype == JOIN_LEFT ||
+ 				 node->js.jointype == JOIN_LEFT_UNIQUE ||
  				 node->js.jointype == JOIN_ANTI))
  			{
  				/*
*************** ExecNestLoop(NestLoopState *node)
*** 247,256 ****
  			}
  
  			/*
! 			 * In a semijoin, we'll consider returning the first match, but
! 			 * after that we're done with this outer tuple.
  			 */
! 			if (node->js.jointype == JOIN_SEMI)
  				node->nl_NeedNewOuter = true;
  
  			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
--- 248,260 ----
  			}
  
  			/*
! 			 * In an inner unique, left unique or semi join, we'll consider
! 			 * returning the first match, but after that we're done with this
! 			 * outer tuple.
  			 */
! 			if (node->js.jointype == JOIN_INNER_UNIQUE ||
! 				node->js.jointype == JOIN_LEFT_UNIQUE ||
! 				node->js.jointype == JOIN_SEMI)
  				node->nl_NeedNewOuter = true;
  
  			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
*************** ExecInitNestLoop(NestLoop *node, EState 
*** 355,363 ****
--- 359,369 ----
  	switch (node->join.jointype)
  	{
  		case JOIN_INNER:
+ 		case JOIN_INNER_UNIQUE:
  		case JOIN_SEMI:
  			break;
  		case JOIN_LEFT:
+ 		case JOIN_LEFT_UNIQUE:
  		case JOIN_ANTI:
  			nlstate->nl_NullInnerTupleSlot =
  				ExecInitNullTupleSlot(estate,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index a21928b..a2a91e5 100644
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copySpecialJoinInfo(const SpecialJoinIn
*** 2065,2070 ****
--- 2065,2071 ----
  	COPY_BITMAPSET_FIELD(syn_lefthand);
  	COPY_BITMAPSET_FIELD(syn_righthand);
  	COPY_SCALAR_FIELD(jointype);
+ 	COPY_SCALAR_FIELD(optimal_jointype);
  	COPY_SCALAR_FIELD(lhs_strict);
  	COPY_SCALAR_FIELD(delay_upper_joins);
  	COPY_SCALAR_FIELD(semi_can_btree);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3c6c567..3302a69 100644
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalSpecialJoinInfo(const SpecialJoinI
*** 837,842 ****
--- 837,843 ----
  	COMPARE_BITMAPSET_FIELD(syn_lefthand);
  	COMPARE_BITMAPSET_FIELD(syn_righthand);
  	COMPARE_SCALAR_FIELD(jointype);
+ 	COMPARE_SCALAR_FIELD(optimal_jointype);
  	COMPARE_SCALAR_FIELD(lhs_strict);
  	COMPARE_SCALAR_FIELD(delay_upper_joins);
  	COMPARE_SCALAR_FIELD(semi_can_btree);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index f783a49..06323d4 100644
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outSpecialJoinInfo(StringInfo str, cons
*** 2276,2281 ****
--- 2276,2282 ----
  	WRITE_BITMAPSET_FIELD(syn_lefthand);
  	WRITE_BITMAPSET_FIELD(syn_righthand);
  	WRITE_ENUM_FIELD(jointype, JoinType);
+ 	WRITE_ENUM_FIELD(optimal_jointype, JoinType);
  	WRITE_BOOL_FIELD(lhs_strict);
  	WRITE_BOOL_FIELD(delay_upper_joins);
  	WRITE_BOOL_FIELD(semi_can_btree);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 70a4c27..e4d5cc9 100644
*** a/src/backend/optimizer/path/costsize.c
--- b/src/backend/optimizer/path/costsize.c
*************** cost_group(Path *path, PlannerInfo *root
*** 1893,1900 ****
   * estimate and getting a tight lower bound.  We choose to not examine the
   * join quals here, since that's by far the most expensive part of the
   * calculations.  The end result is that CPU-cost considerations must be
!  * left for the second phase; and for SEMI/ANTI joins, we must also postpone
!  * incorporation of the inner path's run cost.
   *
   * 'workspace' is to be filled with startup_cost, total_cost, and perhaps
   *		other data to be used by final_cost_nestloop
--- 1893,1901 ----
   * estimate and getting a tight lower bound.  We choose to not examine the
   * join quals here, since that's by far the most expensive part of the
   * calculations.  The end result is that CPU-cost considerations must be
!  * left for the second phase; and for INNER_UNIQUE, LEFT_UNIQUE, SEMI, and
!  * ANTI joins, we must also postpone incorporation of the inner path's run
!  * cost.
   *
   * 'workspace' is to be filled with startup_cost, total_cost, and perhaps
   *		other data to be used by final_cost_nestloop
*************** cost_group(Path *path, PlannerInfo *root
*** 1902,1908 ****
   * 'outer_path' is the outer input to the join
   * 'inner_path' is the inner input to the join
   * 'sjinfo' is extra info about the join for selectivity estimation
!  * 'semifactors' contains valid data if jointype is SEMI or ANTI
   */
  void
  initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
--- 1903,1910 ----
   * 'outer_path' is the outer input to the join
   * 'inner_path' is the inner input to the join
   * 'sjinfo' is extra info about the join for selectivity estimation
!  * 'semifactors' contains valid data if jointype is INNER_UNIQUE, LEFT_UNIQUE,
!  * SEMI, or ANTI
   */
  void
  initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
*************** initial_cost_nestloop(PlannerInfo *root,
*** 1940,1949 ****
  	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
  	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
  
! 	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
  	{
  		/*
! 		 * SEMI or ANTI join: executor will stop after first match.
  		 *
  		 * Getting decent estimates requires inspection of the join quals,
  		 * which we choose to postpone to final_cost_nestloop.
--- 1942,1955 ----
  	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
  	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
  
! 	if (jointype == JOIN_INNER_UNIQUE ||
! 		jointype == JOIN_LEFT_UNIQUE ||
! 		jointype == JOIN_SEMI ||
! 		jointype == JOIN_ANTI)
  	{
  		/*
! 		 * INNER_UNIQUE, LEFT_UNIQUE, SEMI, or ANTI join: executor will stop
! 		 * after first match.
  		 *
  		 * Getting decent estimates requires inspection of the join quals,
  		 * which we choose to postpone to final_cost_nestloop.
*************** initial_cost_nestloop(PlannerInfo *root,
*** 1977,1983 ****
   * 'path' is already filled in except for the rows and cost fields
   * 'workspace' is the result from initial_cost_nestloop
   * 'sjinfo' is extra info about the join for selectivity estimation
!  * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
   */
  void
  final_cost_nestloop(PlannerInfo *root, NestPath *path,
--- 1983,1990 ----
   * 'path' is already filled in except for the rows and cost fields
   * 'workspace' is the result from initial_cost_nestloop
   * 'sjinfo' is extra info about the join for selectivity estimation
!  * 'semifactors' contains valid data if jointype is INNER_UNIQUE, LEFT_UNIQUE,
!  * SEMI, or ANTI
   */
  void
  final_cost_nestloop(PlannerInfo *root, NestPath *path,
*************** final_cost_nestloop(PlannerInfo *root, N
*** 2017,2026 ****
  
  	/* cost of inner-relation source data (we already dealt with outer rel) */
  
! 	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
  	{
  		/*
! 		 * SEMI or ANTI join: executor will stop after first match.
  		 */
  		Cost		inner_run_cost = workspace->inner_run_cost;
  		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
--- 2024,2037 ----
  
  	/* cost of inner-relation source data (we already dealt with outer rel) */
  
! 	if (path->jointype == JOIN_INNER_UNIQUE ||
! 		path->jointype == JOIN_LEFT_UNIQUE ||
! 		path->jointype == JOIN_SEMI ||
! 		path->jointype == JOIN_ANTI)
  	{
  		/*
! 		 * INNER_UNIQUE, LEFT_UNIQUE, SEMI, or ANTI join: executor will stop
! 		 * after first match.
  		 */
  		Cost		inner_run_cost = workspace->inner_run_cost;
  		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
*************** initial_cost_mergejoin(PlannerInfo *root
*** 2250,2255 ****
--- 2261,2267 ----
  			innerendsel = cache->leftendsel;
  		}
  		if (jointype == JOIN_LEFT ||
+ 			jointype == JOIN_LEFT_UNIQUE ||
  			jointype == JOIN_ANTI)
  		{
  			outerstartsel = 0.0;
*************** initial_cost_hashjoin(PlannerInfo *root,
*** 2773,2779 ****
   *		num_batches
   * 'workspace' is the result from initial_cost_hashjoin
   * 'sjinfo' is extra info about the join for selectivity estimation
!  * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
   */
  void
  final_cost_hashjoin(PlannerInfo *root, HashPath *path,
--- 2785,2792 ----
   *		num_batches
   * 'workspace' is the result from initial_cost_hashjoin
   * 'sjinfo' is extra info about the join for selectivity estimation
!  * 'semifactors' contains valid data if path->jointype is INNER_UNIQUE,
!  * LEFT_UNIQUE, SEMI or ANTI
   */
  void
  final_cost_hashjoin(PlannerInfo *root, HashPath *path,
*************** final_cost_hashjoin(PlannerInfo *root, H
*** 2896,2908 ****
  
  	/* CPU costs */
  
! 	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
  	{
  		double		outer_matched_rows;
  		Selectivity inner_scan_frac;
  
  		/*
! 		 * SEMI or ANTI join: executor will stop after first match.
  		 *
  		 * For an outer-rel row that has at least one match, we can expect the
  		 * bucket scan to stop after a fraction 1/(match_count+1) of the
--- 2909,2925 ----
  
  	/* CPU costs */
  
! 	if (path->jpath.jointype == JOIN_INNER_UNIQUE ||
! 		path->jpath.jointype == JOIN_LEFT_UNIQUE ||
! 		path->jpath.jointype == JOIN_SEMI ||
! 		path->jpath.jointype == JOIN_ANTI)
  	{
  		double		outer_matched_rows;
  		Selectivity inner_scan_frac;
  
  		/*
! 		 * INNER_UNIQUE, LEFT_UNIQUE, SEMI or ANTI join: executor will stop
! 		 * after first match.
  		 *
  		 * For an outer-rel row that has at least one match, we can expect the
  		 * bucket scan to stop after a fraction 1/(match_count+1) of the
*************** final_cost_hashjoin(PlannerInfo *root, H
*** 2937,2946 ****
  			clamp_row_est(inner_path_rows / virtualbuckets) * 0.05;
  
  		/* Get # of tuples that will pass the basic join */
! 		if (path->jpath.jointype == JOIN_SEMI)
! 			hashjointuples = outer_matched_rows;
! 		else
  			hashjointuples = outer_path_rows - outer_matched_rows;
  	}
  	else
  	{
--- 2954,2963 ----
  			clamp_row_est(inner_path_rows / virtualbuckets) * 0.05;
  
  		/* Get # of tuples that will pass the basic join */
! 		if (path->jpath.jointype == JOIN_ANTI)
  			hashjointuples = outer_path_rows - outer_matched_rows;
+ 		else
+ 			hashjointuples = outer_matched_rows;
  	}
  	else
  	{
*************** get_restriction_qual_cost(PlannerInfo *r
*** 3469,3481 ****
  
  /*
   * compute_semi_anti_join_factors
!  *	  Estimate how much of the inner input a SEMI or ANTI join
!  *	  can be expected to scan.
   *
!  * In a hash or nestloop SEMI/ANTI join, the executor will stop scanning
!  * inner rows as soon as it finds a match to the current outer row.
!  * We should therefore adjust some of the cost components for this effect.
!  * This function computes some estimates needed for these adjustments.
   * These estimates will be the same regardless of the particular paths used
   * for the outer and inner relation, so we compute these once and then pass
   * them to all the join cost estimation functions.
--- 3486,3498 ----
  
  /*
   * compute_semi_anti_join_factors
!  *	  Estimate how much of the inner input a INNER_UNIQUE, LEFT_UNIQUE, SEMI,
!  *	  ANTI join can be expected to scan.
   *
!  * In a hash or nestloop INNER_UNIQUE/LEFT_UNIQUE/SEMI/ANTI join, the executor
!  * will stop scanning inner rows as soon as it finds a match to the current
!  * outer row. We should therefore adjust some of the cost components for this
!  * effect. This function computes some estimates needed for these adjustments.
   * These estimates will be the same regardless of the particular paths used
   * for the outer and inner relation, so we compute these once and then pass
   * them to all the join cost estimation functions.
*************** get_restriction_qual_cost(PlannerInfo *r
*** 3483,3489 ****
   * Input parameters:
   *	outerrel: outer relation under consideration
   *	innerrel: inner relation under consideration
!  *	jointype: must be JOIN_SEMI or JOIN_ANTI
   *	sjinfo: SpecialJoinInfo relevant to this join
   *	restrictlist: join quals
   * Output parameters:
--- 3500,3507 ----
   * Input parameters:
   *	outerrel: outer relation under consideration
   *	innerrel: inner relation under consideration
!  *	jointype: must be JOIN_INNER_UNIQUE, JOIN_LEFT_UNIQUE,JOIN_SEMI or
!  *	JOIN_ANTI
   *	sjinfo: SpecialJoinInfo relevant to this join
   *	restrictlist: join quals
   * Output parameters:
*************** compute_semi_anti_join_factors(PlannerIn
*** 3506,3512 ****
  	ListCell   *l;
  
  	/* Should only be called in these cases */
! 	Assert(jointype == JOIN_SEMI || jointype == JOIN_ANTI);
  
  	/*
  	 * In an ANTI join, we must ignore clauses that are "pushed down", since
--- 3524,3533 ----
  	ListCell   *l;
  
  	/* Should only be called in these cases */
! 	Assert(jointype == JOIN_INNER_UNIQUE ||
! 		   jointype == JOIN_LEFT_UNIQUE ||
! 		   jointype == JOIN_SEMI ||
! 		   jointype == JOIN_ANTI);
  
  	/*
  	 * In an ANTI join, we must ignore clauses that are "pushed down", since
*************** compute_semi_anti_join_factors(PlannerIn
*** 3530,3536 ****
  		joinquals = restrictlist;
  
  	/*
! 	 * Get the JOIN_SEMI or JOIN_ANTI selectivity of the join clauses.
  	 */
  	jselec = clauselist_selectivity(root,
  									joinquals,
--- 3551,3558 ----
  		joinquals = restrictlist;
  
  	/*
! 	 * Get the JOIN_INNER_UNIQUE, JOIN_LEFT_UNIQUE, JOIN_SEMI or JOIN_ANTI
! 	 * selectivity of the join clauses.
  	 */
  	jselec = clauselist_selectivity(root,
  									joinquals,
*************** calc_joinrel_size_estimate(PlannerInfo *
*** 3967,3974 ****
  	 * pushed-down quals are applied after the outer join, so their
  	 * selectivity applies fully.
  	 *
! 	 * For JOIN_SEMI and JOIN_ANTI, the selectivity is defined as the fraction
! 	 * of LHS rows that have matches, and we apply that straightforwardly.
  	 */
  	switch (jointype)
  	{
--- 3989,4001 ----
  	 * pushed-down quals are applied after the outer join, so their
  	 * selectivity applies fully.
  	 *
! 	 * For JOIN_INNER_UNIQUE, JOIN_SEMI and JOIN_ANTI, the selectivity is
! 	 * defined as the fraction of LHS rows that have matches, and we apply
! 	 * that straightforwardly.
! 	 *
! 	 * For JOIN_SEMI_LEFT, the selectivity is defined as the fraction of the
! 	 * LHS rows that have matches, although unlike JOIN_SEMI we must consider
! 	 * NULL RHS rows, and take the higher estimate of the two.
  	 */
  	switch (jointype)
  	{
*************** calc_joinrel_size_estimate(PlannerInfo *
*** 3990,3998 ****
--- 4017,4032 ----
  			nrows *= pselec;
  			break;
  		case JOIN_SEMI:
+ 		case JOIN_INNER_UNIQUE:
  			nrows = outer_rows * jselec;
  			/* pselec not used */
  			break;
+ 		case JOIN_LEFT_UNIQUE:
+ 			nrows = outer_rows * jselec;
+ 			if (nrows < outer_rows)
+ 				nrows = outer_rows;
+ 			nrows *= pselec;
+ 			break;
  		case JOIN_ANTI:
  			nrows = outer_rows * (1.0 - jselec);
  			nrows *= pselec;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 41b60d0..c92f561 100644
*** a/src/backend/optimizer/path/joinpath.c
--- b/src/backend/optimizer/path/joinpath.c
***************
*** 19,24 ****
--- 19,25 ----
  #include "executor/executor.h"
  #include "foreign/fdwapi.h"
  #include "optimizer/cost.h"
+ #include "optimizer/planmain.h"
  #include "optimizer/pathnode.h"
  #include "optimizer/paths.h"
  
*************** add_paths_to_joinrel(PlannerInfo *root,
*** 88,93 ****
--- 89,101 ----
  	bool		mergejoin_allowed = true;
  	ListCell   *lc;
  
+ 	/*
+ 	 * There may be a more optimal JoinType to use. Check for such cases
+ 	 * first.
+ 	 */
+ 	jointype = get_optimal_jointype(root, outerrel, innerrel, jointype, sjinfo,
+ 									restrictlist);
+ 
  	extra.restrictlist = restrictlist;
  	extra.mergeclause_list = NIL;
  	extra.sjinfo = sjinfo;
*************** add_paths_to_joinrel(PlannerInfo *root,
*** 109,118 ****
  														  &mergejoin_allowed);
  
  	/*
! 	 * If it's SEMI or ANTI join, compute correction factors for cost
! 	 * estimation.  These will be the same for all paths.
  	 */
! 	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
  		compute_semi_anti_join_factors(root, outerrel, innerrel,
  									   jointype, sjinfo, restrictlist,
  									   &extra.semifactors);
--- 117,130 ----
  														  &mergejoin_allowed);
  
  	/*
! 	 * If it's INNER_UNIQUE, LEFT_UNIQUE, SEMI or ANTI join, compute
! 	 * correction factors for cost estimation.  These will be the same for all
! 	 * paths.
  	 */
! 	if (jointype == JOIN_INNER_UNIQUE ||
! 		jointype == JOIN_LEFT_UNIQUE ||
! 		jointype == JOIN_SEMI ||
! 		jointype == JOIN_ANTI)
  		compute_semi_anti_join_factors(root, outerrel, innerrel,
  									   jointype, sjinfo, restrictlist,
  									   &extra.semifactors);
*************** match_unsorted_outer(PlannerInfo *root,
*** 827,842 ****
  	ListCell   *lc1;
  
  	/*
! 	 * Nestloop only supports inner, left, semi, and anti joins.  Also, if we
! 	 * are doing a right or full mergejoin, we must use *all* the mergeclauses
! 	 * as join clauses, else we will not have a valid plan.  (Although these
! 	 * two flags are currently inverses, keep them separate for clarity and
! 	 * possible future changes.)
  	 */
  	switch (jointype)
  	{
  		case JOIN_INNER:
  		case JOIN_LEFT:
  		case JOIN_SEMI:
  		case JOIN_ANTI:
  			nestjoinOK = true;
--- 839,856 ----
  	ListCell   *lc1;
  
  	/*
! 	 * Nestloop only supports inner, inner unique, left, left unique, semi,
! 	 * and anti joins.  Also, if we are doing a right or full mergejoin, we
! 	 * must use *all* the mergeclauses as join clauses, else we will not have
! 	 * a valid plan. (Although these two flags are currently inverses, keep
! 	 * them separate for clarity and possible future changes.)
  	 */
  	switch (jointype)
  	{
  		case JOIN_INNER:
+ 		case JOIN_INNER_UNIQUE:
  		case JOIN_LEFT:
+ 		case JOIN_LEFT_UNIQUE:
  		case JOIN_SEMI:
  		case JOIN_ANTI:
  			nestjoinOK = true;
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 01d4fea..547d09d 100644
*** a/src/backend/optimizer/path/joinrels.c
--- b/src/backend/optimizer/path/joinrels.c
*************** join_is_legal(PlannerInfo *root, RelOptI
*** 490,499 ****
  			/*
  			 * The proposed join could still be legal, but only if we're
  			 * allowed to associate it into the RHS of this SJ.  That means
! 			 * this SJ must be a LEFT join (not SEMI or ANTI, and certainly
! 			 * not FULL) and the proposed join must not overlap the LHS.
  			 */
! 			if (sjinfo->jointype != JOIN_LEFT ||
  				bms_overlap(joinrelids, sjinfo->min_lefthand))
  				return false;	/* invalid join path */
  
--- 490,501 ----
  			/*
  			 * The proposed join could still be legal, but only if we're
  			 * allowed to associate it into the RHS of this SJ.  That means
! 			 * this SJ must be a LEFT or LEFT_UNIQUE join (not SEMI or ANTI,
! 			 * and certainly not FULL) and the proposed join must not overlap
! 			 * the LHS.
  			 */
! 			if ((sjinfo->jointype != JOIN_LEFT &&
! 				 sjinfo->jointype != JOIN_LEFT_UNIQUE) ||
  				bms_overlap(joinrelids, sjinfo->min_lefthand))
  				return false;	/* invalid join path */
  
*************** join_is_legal(PlannerInfo *root, RelOptI
*** 508,515 ****
  	}
  
  	/*
! 	 * Fail if violated any SJ's RHS and didn't match to a LEFT SJ: the
! 	 * proposed join can't associate into an SJ's RHS.
  	 *
  	 * Also, fail if the proposed join's predicate isn't strict; we're
  	 * essentially checking to see if we can apply outer-join identity 3, and
--- 510,517 ----
  	}
  
  	/*
! 	 * Fail if violated any SJ's RHS and didn't match to a LEFT or LEFT_UNIQUE
! 	 * SJ: the proposed join can't associate into an SJ's RHS.
  	 *
  	 * Also, fail if the proposed join's predicate isn't strict; we're
  	 * essentially checking to see if we can apply outer-join identity 3, and
*************** join_is_legal(PlannerInfo *root, RelOptI
*** 518,524 ****
  	 */
  	if (must_be_leftjoin &&
  		(match_sjinfo == NULL ||
! 		 match_sjinfo->jointype != JOIN_LEFT ||
  		 !match_sjinfo->lhs_strict))
  		return false;			/* invalid join path */
  
--- 520,527 ----
  	 */
  	if (must_be_leftjoin &&
  		(match_sjinfo == NULL ||
! 		 (match_sjinfo->jointype != JOIN_LEFT &&
! 		  match_sjinfo->jointype != JOIN_LEFT_UNIQUE) ||
  		 !match_sjinfo->lhs_strict))
  		return false;			/* invalid join path */
  
*************** make_join_rel(PlannerInfo *root, RelOptI
*** 698,703 ****
--- 701,707 ----
  		sjinfo->syn_lefthand = rel1->relids;
  		sjinfo->syn_righthand = rel2->relids;
  		sjinfo->jointype = JOIN_INNER;
+ 		sjinfo->optimal_jointype = JOIN_INNER;
  		/* we don't bother trying to make the remaining fields valid */
  		sjinfo->lhs_strict = false;
  		sjinfo->delay_upper_joins = false;
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 3d305eb..f00d4a1 100644
*** a/src/backend/optimizer/plan/analyzejoins.c
--- b/src/backend/optimizer/plan/analyzejoins.c
***************
*** 34,39 ****
--- 34,41 ----
  
  /* local functions */
  static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+ static bool specialjoin_is_unique_join(PlannerInfo *root,
+ 						   SpecialJoinInfo *sjinfo);
  static void remove_rel_from_query(PlannerInfo *root, int relid,
  					  Relids joinrelids);
  static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
*************** static bool rel_supports_distinctness(Pl
*** 41,46 ****
--- 43,80 ----
  static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
  					List *clause_list);
  static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+ static bool is_innerrel_unique_for(PlannerInfo *root,
+ 					   RelOptInfo *outerrel,
+ 					   RelOptInfo *innerrel,
+ 					   List *restrictlist);
+ 
+ 
+ /*
+  * optimize_outerjoin_types
+  *		Determine the most optimal JoinType for each outer join, and update
+  *		SpecialJoinInfo's optimal_jointype to that join type.
+  */
+ void
+ optimize_outerjoin_types(PlannerInfo *root)
+ {
+ 	ListCell   *lc;
+ 
+ 	foreach(lc, root->join_info_list)
+ 	{
+ 		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+ 
+ 		/*
+ 		 * Currently we're only interested in LEFT JOINs. If we can prove
+ 		 * these to have a unique inner side, based on the join condition then
+ 		 * this can save the executor from having to attempt fruitless
+ 		 * searches for subsequent matching outer tuples.
+ 		 */
+ 		if (sjinfo->jointype == JOIN_LEFT &&
+ 			sjinfo->optimal_jointype != JOIN_LEFT_UNIQUE &&
+ 			specialjoin_is_unique_join(root, sjinfo))
+ 			sjinfo->optimal_jointype = JOIN_LEFT_UNIQUE;
+ 	}
+ }
  
  
  /*
*************** restart:
*** 95,100 ****
--- 129,140 ----
  		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
  
  		/*
+ 		 * We may now be able to optimize some joins which we could not
+ 		 * optimize before.  (XXX huh? how would that happen?)
+ 		 */
+ 		optimize_outerjoin_types(root);
+ 
+ 		/*
  		 * Restart the scan.  This is necessary to ensure we find all
  		 * removable joins independently of ordering of the join_info_list
  		 * (note that removal of attr_needed bits may make a join appear
*************** join_is_removable(PlannerInfo *root, Spe
*** 156,186 ****
  	int			innerrelid;
  	RelOptInfo *innerrel;
  	Relids		joinrelids;
- 	List	   *clause_list = NIL;
- 	ListCell   *l;
  	int			attroff;
  
  	/*
! 	 * Must be a non-delaying left join to a single baserel, else we aren't
! 	 * going to be able to do anything with it.
  	 */
! 	if (sjinfo->jointype != JOIN_LEFT ||
  		sjinfo->delay_upper_joins)
  		return false;
  
  	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
! 		return false;
  
  	innerrel = find_base_rel(root, innerrelid);
  
- 	/*
- 	 * Before we go to the effort of checking whether any innerrel variables
- 	 * are needed above the join, make a quick check to eliminate cases in
- 	 * which we will surely be unable to prove uniqueness of the innerrel.
- 	 */
- 	if (!rel_supports_distinctness(root, innerrel))
- 		return false;
- 
  	/* Compute the relid set for the join we are considering */
  	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
  
--- 196,219 ----
  	int			innerrelid;
  	RelOptInfo *innerrel;
  	Relids		joinrelids;
  	int			attroff;
+ 	ListCell   *l;
  
  	/*
! 	 * Join must have a unique inner side and must be a non-delaying join to a
! 	 * single baserel, else we aren't going to be able to do anything with it.
  	 */
! 	if (sjinfo->optimal_jointype != JOIN_LEFT_UNIQUE ||
  		sjinfo->delay_upper_joins)
  		return false;
  
+ 	/* Now we know specialjoin_is_unique_join() succeeded for this join */
+ 
  	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
! 		return false;			/* should not happen */
  
  	innerrel = find_base_rel(root, innerrelid);
  
  	/* Compute the relid set for the join we are considering */
  	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
  
*************** join_is_removable(PlannerInfo *root, Spe
*** 190,196 ****
  	 *
  	 * Note that this test only detects use of inner-rel attributes in higher
  	 * join conditions and the target list.  There might be such attributes in
! 	 * pushed-down conditions at this join, too.  We check that case below.
  	 *
  	 * As a micro-optimization, it seems better to start with max_attr and
  	 * count down rather than starting with min_attr and counting up, on the
--- 223,230 ----
  	 *
  	 * Note that this test only detects use of inner-rel attributes in higher
  	 * join conditions and the target list.  There might be such attributes in
! 	 * pushed-down conditions at this join, too, but in this case the join
! 	 * would not have been optimized into a LEFT_UNIQUE join.
  	 *
  	 * As a micro-optimization, it seems better to start with max_attr and
  	 * count down rather than starting with min_attr and counting up, on the
*************** join_is_removable(PlannerInfo *root, Spe
*** 231,236 ****
--- 265,305 ----
  			return false;		/* it does reference innerrel */
  	}
  
+ 	return true;
+ }
+ 
+ /*
+  * specialjoin_is_unique_join
+  *		True if it can be proved that this special join can only ever match at
+  *		most one inner row for any single outer row. False is returned if
+  *		there's insufficient evidence to prove the join is unique.
+  */
+ static bool
+ specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+ {
+ 	int			innerrelid;
+ 	RelOptInfo *innerrel;
+ 	Relids		joinrelids;
+ 	List	   *clause_list = NIL;
+ 	ListCell   *lc;
+ 
+ 	/* if there's more than one relation involved, then punt */
+ 	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+ 		return false;
+ 
+ 	innerrel = find_base_rel(root, innerrelid);
+ 
+ 	/*
+ 	 * Before we go to the effort of pulling out the join condition's columns,
+ 	 * make a quick check to eliminate cases in which we will surely be unable
+ 	 * to prove uniqueness of the innerrel.
+ 	 */
+ 	if (!rel_supports_distinctness(root, innerrel))
+ 		return false;
+ 
+ 	/* Compute the relid set for the join we are considering */
+ 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+ 
  	/*
  	 * Search for mergejoinable clauses that constrain the inner rel against
  	 * either the outer rel or a pseudoconstant.  If an operator is
*************** join_is_removable(PlannerInfo *root, Spe
*** 238,246 ****
  	 * it's what we want.  The mergejoinability test also eliminates clauses
  	 * containing volatile functions, which we couldn't depend on.
  	 */
! 	foreach(l, innerrel->joininfo)
  	{
! 		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(l);
  
  		/*
  		 * If it's not a join clause for this outer join, we can't use it.
--- 307,315 ----
  	 * it's what we want.  The mergejoinability test also eliminates clauses
  	 * containing volatile functions, which we couldn't depend on.
  	 */
! 	foreach(lc, innerrel->joininfo)
  	{
! 		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
  
  		/*
  		 * If it's not a join clause for this outer join, we can't use it.
*************** join_is_removable(PlannerInfo *root, Spe
*** 252,261 ****
  			!bms_equal(restrictinfo->required_relids, joinrelids))
  		{
  			/*
! 			 * If such a clause actually references the inner rel then join
! 			 * removal has to be disallowed.  We have to check this despite
! 			 * the previous attr_needed checks because of the possibility of
! 			 * pushed-down clauses referencing the rel.
  			 */
  			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
  				return false;
--- 321,331 ----
  			!bms_equal(restrictinfo->required_relids, joinrelids))
  		{
  			/*
! 			 * If such a clause actually references the inner rel then we
! 			 * can't say we're unique.  (XXX this is confusing conditions for
! 			 * join removability with conditions for uniqueness, and could
! 			 * probably stand to be improved.  But for the moment, keep on
! 			 * applying the stricter condition.)
  			 */
  			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
  				return false;
*************** distinct_col_search(int colno, List *col
*** 837,839 ****
--- 907,1074 ----
  	}
  	return InvalidOid;
  }
+ 
+ 
+ /*
+  * is_innerrel_unique_for
+  *	  Determine if this innerrel can, at most, return a single tuple for each
+  *	  outer tuple, based on the 'restrictlist'.
+  */
+ static bool
+ is_innerrel_unique_for(PlannerInfo *root,
+ 					   RelOptInfo *outerrel,
+ 					   RelOptInfo *innerrel,
+ 					   List *restrictlist)
+ {
+ 	List	   *clause_list = NIL;
+ 	ListCell   *lc;
+ 
+ 	/* Fall out quickly if we certainly can't prove anything */
+ 	if (restrictlist == NIL ||
+ 		!rel_supports_distinctness(root, innerrel))
+ 		return false;
+ 
+ 	/*
+ 	 * Search for mergejoinable clauses that constrain the inner rel against
+ 	 * the outer rel.  If an operator is mergejoinable then it behaves like
+ 	 * equality for some btree opclass, so it's what we want.  The
+ 	 * mergejoinability test also eliminates clauses containing volatile
+ 	 * functions, which we couldn't depend on.
+ 	 */
+ 	foreach(lc, restrictlist)
+ 	{
+ 		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+ 
+ 		/* Ignore if it's not a mergejoinable clause */
+ 		if (!restrictinfo->can_join ||
+ 			restrictinfo->mergeopfamilies == NIL)
+ 			continue;			/* not mergejoinable */
+ 
+ 		/*
+ 		 * Check if clause has the form "outer op inner" or "inner op outer",
+ 		 * and if so mark which side is inner.
+ 		 */
+ 		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+ 									 innerrel->relids))
+ 			continue;			/* no good for these input relations */
+ 
+ 		/* OK, add to list */
+ 		clause_list = lappend(clause_list, restrictinfo);
+ 	}
+ 
+ 	/* Let rel_is_distinct_for() do the hard work */
+ 	return rel_is_distinct_for(root, innerrel, clause_list);
+ }
+ 
+ /*
+  * get_optimal_jointype
+  *	  We may be able to optimize some joins by converting the JoinType to one
+  *	  which the executor is able to run more efficiently. Here we look for
+  *	  such cases and if we find a better choice, then we'll return it,
+  *	  otherwise we'll return the original JoinType.
+  */
+ JoinType
+ get_optimal_jointype(PlannerInfo *root,
+ 					 RelOptInfo *outerrel,
+ 					 RelOptInfo *innerrel,
+ 					 JoinType jointype,
+ 					 SpecialJoinInfo *sjinfo,
+ 					 List *restrictlist)
+ {
+ 	int			innerrelid;
+ 
+ 	/* left joins were already optimized in optimize_outerjoin_types() */
+ 	if (jointype == JOIN_LEFT)
+ 		return sjinfo->optimal_jointype;
+ 
+ 	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+ 		return jointype;
+ 
+ 	/*
+ 	 * Any INNER JOINs which can be proven to return at most one inner tuple
+ 	 * for each outer tuple can be converted in to a JOIN_SEMI.
+ 	 */
+ 	if (jointype == JOIN_INNER)
+ 	{
+ 		MemoryContext old_context;
+ 		ListCell   *lc;
+ 
+ 		/* can't optimize jointype with an empty restrictlist */
+ 		if (restrictlist == NIL)
+ 			return jointype;
+ 
+ 		/*
+ 		 * First let's query the unique and non-unique caches to see if we've
+ 		 * managed to prove that innerrel is unique for some subset of this
+ 		 * outerrel. We don't need an exact match, as if we have any extra
+ 		 * outerrels than were previously cached, then they can't make the
+ 		 * innerrel any less unique.
+ 		 */
+ 		foreach(lc, root->unique_rels[innerrelid])
+ 		{
+ 			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+ 
+ 			if (bms_is_subset(unique_rels, outerrel->relids))
+ 				return JOIN_INNER_UNIQUE;		/* Success! */
+ 		}
+ 
+ 		/*
+ 		 * We may have previously determined that this outerrel, or some
+ 		 * superset thereof, cannot prove this innerrel to be unique.
+ 		 */
+ 		foreach(lc, root->non_unique_rels[innerrelid])
+ 		{
+ 			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+ 
+ 			if (bms_is_subset(outerrel->relids, unique_rels))
+ 				return jointype;
+ 		}
+ 
+ 		/* No cached information, so try to make the proof. */
+ 		if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+ 		{
+ 			jointype = JOIN_INNER_UNIQUE;		/* Success! */
+ 
+ 			/*
+ 			 * Cache the positive result for future probes, being sure to keep
+ 			 * it in the planner_cxt even if we are working in GEQO.
+ 			 *
+ 			 * Note: one might consider trying to isolate the minimal subset
+ 			 * of the outerrels that proved the innerrel unique.  But it's not
+ 			 * worth the trouble, because the planner builds up joinrels
+ 			 * incrementally and so we'll see the minimally sufficient
+ 			 * outerrels before any supersets of them anyway.
+ 			 */
+ 			old_context = MemoryContextSwitchTo(root->planner_cxt);
+ 			root->unique_rels[innerrelid] =
+ 				lappend(root->unique_rels[innerrelid],
+ 						bms_copy(outerrel->relids));
+ 			MemoryContextSwitchTo(old_context);
+ 		}
+ 		else
+ 		{
+ 			/*
+ 			 * None of the join conditions for outerrel proved innerrel
+ 			 * unique, so we can safely reject this outerrel or any subset of
+ 			 * it in future checks.
+ 			 *
+ 			 * However, in normal planning mode, caching this knowledge is
+ 			 * totally pointless; it won't be queried again, because we build
+ 			 * up joinrels from smaller to larger.  It is useful in GEQO mode,
+ 			 * where the knowledge can be carried across successive planning
+ 			 * attempts; and it's likely to be useful when using join-search
+ 			 * plugins, too.  Hence cache only when join_search_private is
+ 			 * non-NULL.  (Yeah, that's a hack, but it seems reasonable.)
+ 			 */
+ 			if (root->join_search_private)
+ 			{
+ 				old_context = MemoryContextSwitchTo(root->planner_cxt);
+ 				root->non_unique_rels[innerrelid] =
+ 					lappend(root->non_unique_rels[innerrelid],
+ 							bms_copy(outerrel->relids));
+ 				MemoryContextSwitchTo(old_context);
+ 			}
+ 		}
+ 	}
+ 	return jointype;
+ }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 9999eea..a615680 100644
*** a/src/backend/optimizer/plan/initsplan.c
--- b/src/backend/optimizer/plan/initsplan.c
*************** make_outerjoininfo(PlannerInfo *root,
*** 1128,1133 ****
--- 1128,1135 ----
  	sjinfo->syn_lefthand = left_rels;
  	sjinfo->syn_righthand = right_rels;
  	sjinfo->jointype = jointype;
+ 	/* this may be changed later */
+ 	sjinfo->optimal_jointype = jointype;
  	/* this always starts out false */
  	sjinfo->delay_upper_joins = false;
  
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index edd95d8..690349d 100644
*** a/src/backend/optimizer/plan/planmain.c
--- b/src/backend/optimizer/plan/planmain.c
*************** query_planner(PlannerInfo *root, List *t
*** 124,129 ****
--- 124,132 ----
  	 */
  	setup_simple_rel_arrays(root);
  
+ 	/* Allocate memory for caching which joins are unique. */
+ 	setup_unique_join_caches(root);
+ 
  	/*
  	 * Construct RelOptInfo nodes for all base relations in query, and
  	 * indirectly for all appendrel member relations ("other rels").  This
*************** query_planner(PlannerInfo *root, List *t
*** 185,190 ****
--- 188,196 ----
  	 */
  	fix_placeholder_input_needed_levels(root);
  
+ 	/* Determine if there's a better JoinType for each outer join */
+ 	optimize_outerjoin_types(root);
+ 
  	/*
  	 * Remove any useless outer joins.  Ideally this would be done during
  	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 5537c14..95c3a2f 100644
*** a/src/backend/optimizer/plan/setrefs.c
--- b/src/backend/optimizer/plan/setrefs.c
*************** set_join_references(PlannerInfo *root, J
*** 1617,1622 ****
--- 1617,1623 ----
  	switch (join->jointype)
  	{
  		case JOIN_LEFT:
+ 		case JOIN_LEFT_UNIQUE:
  		case JOIN_SEMI:
  		case JOIN_ANTI:
  			inner_itlist->has_non_vars = false;
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 802eab3..c11769e 100644
*** a/src/backend/optimizer/util/relnode.c
--- b/src/backend/optimizer/util/relnode.c
*************** setup_simple_rel_arrays(PlannerInfo *roo
*** 81,86 ****
--- 81,102 ----
  }
  
  /*
+  * setup_unique_join_caches
+  *	  Prepare the arrays we use for caching which joins are proved to be
+  *	  unique and non-unique.
+  */
+ void
+ setup_unique_join_caches(PlannerInfo *root)
+ {
+ 	int			size = list_length(root->parse->rtable) + 1;
+ 
+ 	/* initialize the unique relation cache to all NULLs */
+ 	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+ 
+ 	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+ }
+ 
+ /*
   * build_simple_rel
   *	  Construct a new RelOptInfo for a base relation or 'other' relation.
   */
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 751de4b..66eb670 100644
*** a/src/backend/parser/parse_clause.c
--- b/src/backend/parser/parse_clause.c
*************** transformFromClauseItem(ParseState *psta
*** 978,988 ****
  		/*
  		 * Make the left-side RTEs available for LATERAL access within the
  		 * right side, by temporarily adding them to the pstate's namespace
! 		 * list.  Per SQL:2008, if the join type is not INNER or LEFT then the
! 		 * left-side names must still be exposed, but it's an error to
! 		 * reference them.  (Stupid design, but that's what it says.)  Hence,
! 		 * we always push them into the namespace, but mark them as not
! 		 * lateral_ok if the jointype is wrong.
  		 *
  		 * Notice that we don't require the merged namespace list to be
  		 * conflict-free.  See the comments for scanNameSpaceForRefname().
--- 978,988 ----
  		/*
  		 * Make the left-side RTEs available for LATERAL access within the
  		 * right side, by temporarily adding them to the pstate's namespace
! 		 * list.  Per SQL:2008, if the join type is not INNER, INNER_UNIQUE,
! 		 * LEFT or LEFT_UNIQUE then the left-side names must still be exposed,
! 		 * but it's an error to reference them.  (Stupid design, but that's
! 		 * what it says.)  Hence, we always push them into the namespace, but
! 		 * mark them as not lateral_ok if the jointype is wrong.
  		 *
  		 * Notice that we don't require the merged namespace list to be
  		 * conflict-free.  See the comments for scanNameSpaceForRefname().
*************** transformFromClauseItem(ParseState *psta
*** 990,996 ****
  		 * NB: this coding relies on the fact that list_concat is not
  		 * destructive to its second argument.
  		 */
! 		lateral_ok = (j->jointype == JOIN_INNER || j->jointype == JOIN_LEFT);
  		setNamespaceLateralState(l_namespace, true, lateral_ok);
  
  		sv_namespace_length = list_length(pstate->p_namespace);
--- 990,999 ----
  		 * NB: this coding relies on the fact that list_concat is not
  		 * destructive to its second argument.
  		 */
! 		lateral_ok = (j->jointype == JOIN_INNER ||
! 					  j->jointype == JOIN_INNER_UNIQUE ||
! 					  j->jointype == JOIN_LEFT ||
! 					  j->jointype == JOIN_LEFT_UNIQUE);
  		setNamespaceLateralState(l_namespace, true, lateral_ok);
  
  		sv_namespace_length = list_length(pstate->p_namespace);
diff --git a/src/backend/utils/adt/network_selfuncs.c b/src/backend/utils/adt/network_selfuncs.c
index 2e39687..f14a3cb 100644
*** a/src/backend/utils/adt/network_selfuncs.c
--- b/src/backend/utils/adt/network_selfuncs.c
*************** networkjoinsel(PG_FUNCTION_ARGS)
*** 215,221 ****
--- 215,223 ----
  	switch (sjinfo->jointype)
  	{
  		case JOIN_INNER:
+ 		case JOIN_INNER_UNIQUE:
  		case JOIN_LEFT:
+ 		case JOIN_LEFT_UNIQUE:
  		case JOIN_FULL:
  
  			/*
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index cc2a9a1..346952e 100644
*** a/src/backend/utils/adt/selfuncs.c
--- b/src/backend/utils/adt/selfuncs.c
*************** eqjoinsel(PG_FUNCTION_ARGS)
*** 2202,2207 ****
--- 2202,2208 ----
  	{
  		case JOIN_INNER:
  		case JOIN_LEFT:
+ 		case JOIN_LEFT_UNIQUE:
  		case JOIN_FULL:
  			selec = eqjoinsel_inner(operator, &vardata1, &vardata2);
  			break;
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 84efa8e..2ba7b07 100644
*** a/src/include/nodes/nodes.h
--- b/src/include/nodes/nodes.h
*************** typedef enum JoinType
*** 640,645 ****
--- 640,655 ----
  	JOIN_ANTI,					/* 1 copy of each LHS row that has no match */
  
  	/*
+ 	 * The following join types are a variants of JOIN_INNER and JOIN_LEFT for
+ 	 * when the inner side of the join is known to be unique. This serves
+ 	 * solely as an optimization to allow the executor to skip looking for
+ 	 * another matching tuple in the inner side, when it's known that another
+ 	 * cannot exist.
+ 	 */
+ 	JOIN_INNER_UNIQUE,
+ 	JOIN_LEFT_UNIQUE,
+ 
+ 	/*
  	 * These codes are used internally in the planner, but are not supported
  	 * by the executor (nor, indeed, by most of the planner).
  	 */
*************** typedef enum JoinType
*** 668,673 ****
--- 678,684 ----
  #define IS_OUTER_JOIN(jointype) \
  	(((1 << (jointype)) & \
  	  ((1 << JOIN_LEFT) | \
+ 	   (1 << JOIN_LEFT_UNIQUE) | \
  	   (1 << JOIN_FULL) | \
  	   (1 << JOIN_RIGHT) | \
  	   (1 << JOIN_ANTI))) != 0)
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index d430f6e..b00aba3 100644
*** a/src/include/nodes/relation.h
--- b/src/include/nodes/relation.h
*************** typedef struct PlannerInfo
*** 221,226 ****
--- 221,238 ----
  	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
  	int			join_cur_level; /* index of list being extended */
  
+ 	/*
+ 	 * During the join search we attempt to optimize joins to try to prove
+ 	 * their inner side to be unique based on the join condition. This is a
+ 	 * rather expensive thing to do as it requires checking each relations
+ 	 * unique indexes to see if the relation can, at most, return one tuple
+ 	 * for each outer tuple. We use this cache during the join search to
+ 	 * record lists of the sets of relations which both prove, and disprove
+ 	 * the uniqueness properties for the relid indexed by these arrays.
+ 	 */
+ 	List	  **unique_rels;	/* cache for proven unique rels */
+ 	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+ 
  	List	   *init_plans;		/* init SubPlans for query */
  
  	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
*************** typedef struct SpecialJoinInfo
*** 1750,1755 ****
--- 1762,1769 ----
  	Relids		syn_lefthand;	/* base relids syntactically within LHS */
  	Relids		syn_righthand;	/* base relids syntactically within RHS */
  	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
+ 	JoinType	optimal_jointype;		/* as above but may also be
+ 										 * INNER_UNIQUE or LEFT_UNIQUE */
  	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
  	bool		delay_upper_joins;		/* can't commute with upper RHS */
  	/* Remaining fields are set only for JOIN_SEMI jointype: */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index acc827d..13b212f 100644
*** a/src/include/optimizer/pathnode.h
--- b/src/include/optimizer/pathnode.h
*************** extern Path *reparameterize_path(Planner
*** 236,241 ****
--- 236,242 ----
   * prototypes for relnode.c
   */
  extern void setup_simple_rel_arrays(PlannerInfo *root);
+ extern void setup_unique_join_caches(PlannerInfo *root);
  extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
  				 RelOptKind reloptkind);
  extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index da9c640..a10146c 100644
*** a/src/include/optimizer/planmain.h
--- b/src/include/optimizer/planmain.h
*************** extern RestrictInfo *build_implied_join_
*** 99,107 ****
--- 99,114 ----
  /*
   * prototypes for plan/analyzejoins.c
   */
+ extern void optimize_outerjoin_types(PlannerInfo *root);
  extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
  extern bool query_supports_distinctness(Query *query);
  extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
+ extern JoinType get_optimal_jointype(PlannerInfo *root,
+ 					 RelOptInfo *outerrel,
+ 					 RelOptInfo *innerrel,
+ 					 JoinType jointype,
+ 					 SpecialJoinInfo *sjinfo,
+ 					 List *restrictlist);
  
  /*
   * prototypes for plan/setrefs.c
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 3ff6691..691fa11 100644
*** a/src/test/regress/expected/aggregates.out
--- b/src/test/regress/expected/aggregates.out
*************** explain (costs off) select a,c from t1 g
*** 880,908 ****
  explain (costs off) select *
  from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
  group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
!                       QUERY PLAN                       
! -------------------------------------------------------
!  Group
     Group Key: t1.a, t1.b, t2.x, t2.y
!    ->  Merge Join
!          Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
!          ->  Index Scan using t1_pkey on t1
!          ->  Index Scan using t2_pkey on t2
! (6 rows)
  
  -- Test case where t1 can be optimized but not t2
  explain (costs off) select t1.*,t2.x,t2.z
  from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
  group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
!                       QUERY PLAN                       
! -------------------------------------------------------
   HashAggregate
     Group Key: t1.a, t1.b, t2.x, t2.z
!    ->  Merge Join
!          Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
!          ->  Index Scan using t1_pkey on t1
!          ->  Index Scan using t2_pkey on t2
! (6 rows)
  
  -- Cannot optimize when PK is deferrable
  explain (costs off) select * from t3 group by a,b,c;
--- 880,910 ----
  explain (costs off) select *
  from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
  group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
!                       QUERY PLAN                      
! ------------------------------------------------------
!  HashAggregate
     Group Key: t1.a, t1.b, t2.x, t2.y
!    ->  Hash Unique Inner Join
!          Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
!          ->  Seq Scan on t2
!          ->  Hash
!                ->  Seq Scan on t1
! (7 rows)
  
  -- Test case where t1 can be optimized but not t2
  explain (costs off) select t1.*,t2.x,t2.z
  from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
  group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
!                       QUERY PLAN                      
! ------------------------------------------------------
   HashAggregate
     Group Key: t1.a, t1.b, t2.x, t2.z
!    ->  Hash Unique Inner Join
!          Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
!          ->  Seq Scan on t2
!          ->  Hash
!                ->  Seq Scan on t1
! (7 rows)
  
  -- Cannot optimize when PK is deferrable
  explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 0391b8e..f446284 100644
*** a/src/test/regress/expected/equivclass.out
--- b/src/test/regress/expected/equivclass.out
*************** explain (costs off)
*** 186,192 ****
    select * from ec1, ec2 where ff = x1 and x1 = '42'::int8alias2;
                 QUERY PLAN                
  -----------------------------------------
!  Nested Loop
     ->  Seq Scan on ec2
           Filter: (x1 = '42'::int8alias2)
     ->  Index Scan using ec1_pkey on ec1
--- 186,192 ----
    select * from ec1, ec2 where ff = x1 and x1 = '42'::int8alias2;
                 QUERY PLAN                
  -----------------------------------------
!  Nested Loop Unique Inner Join
     ->  Seq Scan on ec2
           Filter: (x1 = '42'::int8alias2)
     ->  Index Scan using ec1_pkey on ec1
*************** explain (costs off)
*** 310,316 ****
           ->  Index Scan using ec1_expr3 on ec1 ec1_5
           ->  Index Scan using ec1_expr4 on ec1 ec1_6
     ->  Materialize
!          ->  Merge Join
                 Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
                 ->  Merge Append
                       Sort Key: (((ec1_1.ff + 2) + 1))
--- 310,316 ----
           ->  Index Scan using ec1_expr3 on ec1 ec1_5
           ->  Index Scan using ec1_expr4 on ec1 ec1_6
     ->  Materialize
!          ->  Merge Unique Inner Join
                 Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
                 ->  Merge Append
                       Sort Key: (((ec1_1.ff + 2) + 1))
*************** explain (costs off)
*** 365,371 ****
    where ss1.x = ec1.f1 and ec1.ff = 42::int8;
                       QUERY PLAN                      
  -----------------------------------------------------
!  Merge Join
     Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
     ->  Merge Append
           Sort Key: (((ec1_1.ff + 2) + 1))
--- 365,371 ----
    where ss1.x = ec1.f1 and ec1.ff = 42::int8;
                       QUERY PLAN                      
  -----------------------------------------------------
!  Merge Unique Inner Join
     Merge Cond: ((((ec1_1.ff + 2) + 1)) = ec1.f1)
     ->  Merge Append
           Sort Key: (((ec1_1.ff + 2) + 1))
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index cafbc5e..7d0aaa7 100644
*** a/src/test/regress/expected/join.out
--- b/src/test/regress/expected/join.out
*************** from nt3 as nt3
*** 2756,2763 ****
  where nt3.id = 1 and ss2.b3;
                    QUERY PLAN                   
  -----------------------------------------------
!  Nested Loop
!    ->  Nested Loop
           ->  Index Scan using nt3_pkey on nt3
                 Index Cond: (id = 1)
           ->  Index Scan using nt2_pkey on nt2
--- 2756,2763 ----
  where nt3.id = 1 and ss2.b3;
                    QUERY PLAN                   
  -----------------------------------------------
!  Nested Loop Unique Inner Join
!    ->  Nested Loop Unique Inner Join
           ->  Index Scan using nt3_pkey on nt3
                 Index Cond: (id = 1)
           ->  Index Scan using nt2_pkey on nt2
*************** explain (costs off)
*** 4035,4041 ****
      on (p.k = ss.k);
             QUERY PLAN            
  ---------------------------------
!  Hash Left Join
     Hash Cond: (p.k = c.k)
     ->  Seq Scan on parent p
     ->  Hash
--- 4035,4041 ----
      on (p.k = ss.k);
             QUERY PLAN            
  ---------------------------------
!  Hash Unique Left Join
     Hash Cond: (p.k = c.k)
     ->  Seq Scan on parent p
     ->  Hash
*************** ERROR:  invalid reference to FROM-clause
*** 5260,5262 ****
--- 5260,5497 ----
  LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                  ^
  HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+ --
+ -- test planner's ability to change joins into their appropriate semi join
+ -- type
+ --
+ create table j1 (id int primary key);
+ create table j2 (id int primary key);
+ create table j3 (id int);
+ insert into j1 values(1),(2),(3);
+ insert into j2 values(1),(2),(3);
+ insert into j3 values(1),(1);
+ analyze j1;
+ analyze j2;
+ analyze j3;
+ -- ensure join is changed to a semi join
+ explain (verbose, costs off)
+ select * from j1 inner join j2 on j1.id = j2.id;
+             QUERY PLAN             
+ -----------------------------------
+  Hash Unique Inner Join
+    Output: j1.id, j2.id
+    Hash Cond: (j1.id = j2.id)
+    ->  Seq Scan on public.j1
+          Output: j1.id
+    ->  Hash
+          Output: j2.id
+          ->  Seq Scan on public.j2
+                Output: j2.id
+ (9 rows)
+ 
+ -- ensure join not changed when not an equi-join
+ explain (verbose, costs off)
+ select * from j1 inner join j2 on j1.id > j2.id;
+             QUERY PLAN             
+ -----------------------------------
+  Nested Loop
+    Output: j1.id, j2.id
+    Join Filter: (j1.id > j2.id)
+    ->  Seq Scan on public.j1
+          Output: j1.id
+    ->  Materialize
+          Output: j2.id
+          ->  Seq Scan on public.j2
+                Output: j2.id
+ (9 rows)
+ 
+ -- don't change, as j3 has no unique index or pk on id
+ explain (verbose, costs off)
+ select * from j1 inner join j3 on j1.id = j3.id;
+             QUERY PLAN             
+ -----------------------------------
+  Hash Unique Inner Join
+    Output: j1.id, j3.id
+    Hash Cond: (j3.id = j1.id)
+    ->  Seq Scan on public.j3
+          Output: j3.id
+    ->  Hash
+          Output: j1.id
+          ->  Seq Scan on public.j1
+                Output: j1.id
+ (9 rows)
+ 
+ -- ensure left join is converted to left semi join
+ explain (verbose, costs off)
+ select * from j1 left join j2 on j1.id = j2.id;
+             QUERY PLAN             
+ -----------------------------------
+  Hash Unique Left Join
+    Output: j1.id, j2.id
+    Hash Cond: (j1.id = j2.id)
+    ->  Seq Scan on public.j1
+          Output: j1.id
+    ->  Hash
+          Output: j2.id
+          ->  Seq Scan on public.j2
+                Output: j2.id
+ (9 rows)
+ 
+ -- ensure right join is converted too
+ explain (verbose, costs off)
+ select * from j1 right join j2 on j1.id = j2.id;
+             QUERY PLAN             
+ -----------------------------------
+  Hash Unique Left Join
+    Output: j1.id, j2.id
+    Hash Cond: (j2.id = j1.id)
+    ->  Seq Scan on public.j2
+          Output: j2.id
+    ->  Hash
+          Output: j1.id
+          ->  Seq Scan on public.j1
+                Output: j1.id
+ (9 rows)
+ 
+ -- a clauseless (cross) join can't be converted
+ explain (verbose, costs off)
+ select * from j1 cross join j2;
+             QUERY PLAN             
+ -----------------------------------
+  Nested Loop
+    Output: j1.id, j2.id
+    ->  Seq Scan on public.j1
+          Output: j1.id
+    ->  Materialize
+          Output: j2.id
+          ->  Seq Scan on public.j2
+                Output: j2.id
+ (8 rows)
+ 
+ -- ensure a natural join is converted to a semi join
+ explain (verbose, costs off)
+ select * from j1 natural join j2;
+             QUERY PLAN             
+ -----------------------------------
+  Hash Unique Inner Join
+    Output: j1.id
+    Hash Cond: (j1.id = j2.id)
+    ->  Seq Scan on public.j1
+          Output: j1.id
+    ->  Hash
+          Output: j2.id
+          ->  Seq Scan on public.j2
+                Output: j2.id
+ (9 rows)
+ 
+ -- ensure distinct clause allows the inner to become a semi join
+ explain (verbose, costs off)
+ select * from j1
+ inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                   QUERY PLAN                   
+ -----------------------------------------------
+  Nested Loop Unique Inner Join
+    Output: j1.id, j3.id
+    Join Filter: (j1.id = j3.id)
+    ->  Seq Scan on public.j1
+          Output: j1.id
+    ->  Materialize
+          Output: j3.id
+          ->  Unique
+                Output: j3.id
+                ->  Sort
+                      Output: j3.id
+                      Sort Key: j3.id
+                      ->  Seq Scan on public.j3
+                            Output: j3.id
+ (14 rows)
+ 
+ -- ensure group by clause allows the inner to become a semi join
+ explain (verbose, costs off)
+ select * from j1
+ inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                   QUERY PLAN                   
+ -----------------------------------------------
+  Nested Loop Unique Inner Join
+    Output: j1.id, j3.id
+    Join Filter: (j1.id = j3.id)
+    ->  Seq Scan on public.j1
+          Output: j1.id
+    ->  Materialize
+          Output: j3.id
+          ->  Group
+                Output: j3.id
+                Group Key: j3.id
+                ->  Sort
+                      Output: j3.id
+                      Sort Key: j3.id
+                      ->  Seq Scan on public.j3
+                            Output: j3.id
+ (15 rows)
+ 
+ -- ensure a full join is not altered
+ explain (verbose, costs off)
+ select * from j1 full join j2 on j1.id = j2.id;
+             QUERY PLAN             
+ -----------------------------------
+  Hash Full Join
+    Output: j1.id, j2.id
+    Hash Cond: (j1.id = j2.id)
+    ->  Seq Scan on public.j1
+          Output: j1.id
+    ->  Hash
+          Output: j2.id
+          ->  Seq Scan on public.j2
+                Output: j2.id
+ (9 rows)
+ 
+ drop table j1;
+ drop table j2;
+ drop table j3;
+ -- test a more complex permutations of join conversions
+ create table j1 (id1 int, id2 int, primary key(id1,id2));
+ create table j2 (id1 int, id2 int, primary key(id1,id2));
+ create table j3 (id1 int, id2 int, primary key(id1,id2));
+ insert into j1 values(1,1),(2,2);
+ insert into j2 values(1,1);
+ insert into j3 values(1,1);
+ analyze j1;
+ analyze j2;
+ analyze j3;
+ -- ensure there's no join conversion when not all columns which are part of
+ -- the unique index are part of the join clause
+ explain (verbose, costs off)
+ select * from j1
+ inner join j2 on j1.id1 = j2.id1;
+                 QUERY PLAN                
+ ------------------------------------------
+  Nested Loop
+    Output: j1.id1, j1.id2, j2.id1, j2.id2
+    Join Filter: (j1.id1 = j2.id1)
+    ->  Seq Scan on public.j2
+          Output: j2.id1, j2.id2
+    ->  Seq Scan on public.j1
+          Output: j1.id1, j1.id2
+ (7 rows)
+ 
+ -- ensure inner is converted to semi join when there's multiple columns in the
+ -- join condition
+ explain (verbose, costs off)
+ select * from j1
+ inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                         QUERY PLAN                        
+ ----------------------------------------------------------
+  Nested Loop Unique Inner Join
+    Output: j1.id1, j1.id2, j2.id1, j2.id2
+    Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+    ->  Seq Scan on public.j1
+          Output: j1.id1, j1.id2
+    ->  Materialize
+          Output: j2.id1, j2.id2
+          ->  Seq Scan on public.j2
+                Output: j2.id1, j2.id2
+ (9 rows)
+ 
+ drop table j1;
+ drop table j2;
+ drop table j3;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 067aa8d..9fdc695 100644
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** EXPLAIN (COSTS OFF) SELECT * FROM docume
*** 276,282 ****
  EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
                       QUERY PLAN                     
  ----------------------------------------------------
!  Nested Loop
     ->  Subquery Scan on document
           Filter: f_leak(document.dtitle)
           ->  Seq Scan on document document_1
--- 276,282 ----
  EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
                       QUERY PLAN                     
  ----------------------------------------------------
!  Nested Loop Unique Inner Join
     ->  Subquery Scan on document
           Filter: f_leak(document.dtitle)
           ->  Seq Scan on document document_1
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 7f57526..2651ff8 100644
*** a/src/test/regress/expected/select_views.out
--- b/src/test/regress/expected/select_views.out
*************** NOTICE:  f_leak => 9801-2345-6789-0123
*** 1411,1417 ****
  EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                         QUERY PLAN                        
  ---------------------------------------------------------
!  Hash Join
     Hash Cond: (r.cid = l.cid)
     ->  Seq Scan on credit_card r
           Filter: f_leak(cnum)
--- 1411,1417 ----
  EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                         QUERY PLAN                        
  ---------------------------------------------------------
!  Hash Unique Inner Join
     Hash Cond: (r.cid = l.cid)
     ->  Seq Scan on credit_card r
           Filter: f_leak(cnum)
*************** EXPLAIN (COSTS OFF) SELECT * FROM my_cre
*** 1432,1438 ****
  ---------------------------------------------------------------
   Subquery Scan on my_credit_card_secure
     Filter: f_leak(my_credit_card_secure.cnum)
!    ->  Hash Join
           Hash Cond: (r.cid = l.cid)
           ->  Seq Scan on credit_card r
           ->  Hash
--- 1432,1438 ----
  ---------------------------------------------------------------
   Subquery Scan on my_credit_card_secure
     Filter: f_leak(my_credit_card_secure.cnum)
!    ->  Hash Unique Inner Join
           Hash Cond: (r.cid = l.cid)
           ->  Seq Scan on credit_card r
           ->  Hash
*************** EXPLAIN (COSTS OFF) SELECT * FROM my_cre
*** 1466,1472 ****
     ->  Materialize
           ->  Subquery Scan on l
                 Filter: f_leak(l.cnum)
!                ->  Hash Join
                       Hash Cond: (r_1.cid = l_1.cid)
                       ->  Seq Scan on credit_card r_1
                       ->  Hash
--- 1466,1472 ----
     ->  Materialize
           ->  Subquery Scan on l
                 Filter: f_leak(l.cnum)
!                ->  Hash Unique Inner Join
                       Hash Cond: (r_1.cid = l_1.cid)
                       ->  Seq Scan on credit_card r_1
                       ->  Hash
*************** EXPLAIN (COSTS OFF) SELECT * FROM my_cre
*** 1497,1503 ****
           ->  Seq Scan on credit_usage r
                 Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
           ->  Materialize
!                ->  Hash Join
                       Hash Cond: (r_1.cid = l.cid)
                       ->  Seq Scan on credit_card r_1
                       ->  Hash
--- 1497,1503 ----
           ->  Seq Scan on credit_usage r
                 Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
           ->  Materialize
!                ->  Hash Unique Inner Join
                       Hash Cond: (r_1.cid = l.cid)
                       ->  Seq Scan on credit_card r_1
                       ->  Hash
diff --git a/src/test/regress/expected/select_views_1.out b/src/test/regress/expected/select_views_1.out
index 5275ef0..81795d8 100644
*** a/src/test/regress/expected/select_views_1.out
--- b/src/test/regress/expected/select_views_1.out
*************** NOTICE:  f_leak => 9801-2345-6789-0123
*** 1411,1417 ****
  EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                         QUERY PLAN                        
  ---------------------------------------------------------
!  Hash Join
     Hash Cond: (r.cid = l.cid)
     ->  Seq Scan on credit_card r
           Filter: f_leak(cnum)
--- 1411,1417 ----
  EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum);
                         QUERY PLAN                        
  ---------------------------------------------------------
!  Hash Unique Inner Join
     Hash Cond: (r.cid = l.cid)
     ->  Seq Scan on credit_card r
           Filter: f_leak(cnum)
*************** EXPLAIN (COSTS OFF) SELECT * FROM my_cre
*** 1432,1438 ****
  ---------------------------------------------------------------
   Subquery Scan on my_credit_card_secure
     Filter: f_leak(my_credit_card_secure.cnum)
!    ->  Hash Join
           Hash Cond: (r.cid = l.cid)
           ->  Seq Scan on credit_card r
           ->  Hash
--- 1432,1438 ----
  ---------------------------------------------------------------
   Subquery Scan on my_credit_card_secure
     Filter: f_leak(my_credit_card_secure.cnum)
!    ->  Hash Unique Inner Join
           Hash Cond: (r.cid = l.cid)
           ->  Seq Scan on credit_card r
           ->  Hash
*************** EXPLAIN (COSTS OFF) SELECT * FROM my_cre
*** 1466,1472 ****
     ->  Materialize
           ->  Subquery Scan on l
                 Filter: f_leak(l.cnum)
!                ->  Hash Join
                       Hash Cond: (r_1.cid = l_1.cid)
                       ->  Seq Scan on credit_card r_1
                       ->  Hash
--- 1466,1472 ----
     ->  Materialize
           ->  Subquery Scan on l
                 Filter: f_leak(l.cnum)
!                ->  Hash Unique Inner Join
                       Hash Cond: (r_1.cid = l_1.cid)
                       ->  Seq Scan on credit_card r_1
                       ->  Hash
*************** EXPLAIN (COSTS OFF) SELECT * FROM my_cre
*** 1497,1503 ****
           ->  Seq Scan on credit_usage r
                 Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
           ->  Materialize
!                ->  Hash Join
                       Hash Cond: (r_1.cid = l.cid)
                       ->  Seq Scan on credit_card r_1
                       ->  Hash
--- 1497,1503 ----
           ->  Seq Scan on credit_usage r
                 Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
           ->  Materialize
!                ->  Hash Unique Inner Join
                       Hash Cond: (r_1.cid = l.cid)
                       ->  Seq Scan on credit_card r_1
                       ->  Hash
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 3430f91..38cc39c 100644
*** a/src/test/regress/sql/join.sql
--- b/src/test/regress/sql/join.sql
*************** update xx1 set x2 = f1 from xx1, lateral
*** 1696,1698 ****
--- 1696,1791 ----
  delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
  delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
  delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+ 
+ --
+ -- test planner's ability to change joins into their appropriate semi join
+ -- type
+ --
+ 
+ create table j1 (id int primary key);
+ create table j2 (id int primary key);
+ create table j3 (id int);
+ 
+ insert into j1 values(1),(2),(3);
+ insert into j2 values(1),(2),(3);
+ insert into j3 values(1),(1);
+ 
+ analyze j1;
+ analyze j2;
+ analyze j3;
+ 
+ -- ensure join is changed to a semi join
+ explain (verbose, costs off)
+ select * from j1 inner join j2 on j1.id = j2.id;
+ 
+ -- ensure join not changed when not an equi-join
+ explain (verbose, costs off)
+ select * from j1 inner join j2 on j1.id > j2.id;
+ 
+ -- don't change, as j3 has no unique index or pk on id
+ explain (verbose, costs off)
+ select * from j1 inner join j3 on j1.id = j3.id;
+ 
+ -- ensure left join is converted to left semi join
+ explain (verbose, costs off)
+ select * from j1 left join j2 on j1.id = j2.id;
+ 
+ -- ensure right join is converted too
+ explain (verbose, costs off)
+ select * from j1 right join j2 on j1.id = j2.id;
+ 
+ -- a clauseless (cross) join can't be converted
+ explain (verbose, costs off)
+ select * from j1 cross join j2;
+ 
+ -- ensure a natural join is converted to a semi join
+ explain (verbose, costs off)
+ select * from j1 natural join j2;
+ 
+ -- ensure distinct clause allows the inner to become a semi join
+ explain (verbose, costs off)
+ select * from j1
+ inner join (select distinct id from j3) j3 on j1.id = j3.id;
+ 
+ -- ensure group by clause allows the inner to become a semi join
+ explain (verbose, costs off)
+ select * from j1
+ inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+ 
+ -- ensure a full join is not altered
+ explain (verbose, costs off)
+ select * from j1 full join j2 on j1.id = j2.id;
+ 
+ drop table j1;
+ drop table j2;
+ drop table j3;
+ 
+ -- test a more complex permutations of join conversions
+ 
+ create table j1 (id1 int, id2 int, primary key(id1,id2));
+ create table j2 (id1 int, id2 int, primary key(id1,id2));
+ create table j3 (id1 int, id2 int, primary key(id1,id2));
+ 
+ insert into j1 values(1,1),(2,2);
+ insert into j2 values(1,1);
+ insert into j3 values(1,1);
+ 
+ analyze j1;
+ analyze j2;
+ analyze j3;
+ 
+ -- ensure there's no join conversion when not all columns which are part of
+ -- the unique index are part of the join clause
+ explain (verbose, costs off)
+ select * from j1
+ inner join j2 on j1.id1 = j2.id1;
+ 
+ -- ensure inner is converted to semi join when there's multiple columns in the
+ -- join condition
+ explain (verbose, costs off)
+ select * from j1
+ inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+ 
+ drop table j1;
+ drop table j2;
+ drop table j3;
#80Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#79)
Re: Performance improvement for joins where outer side is unique

On Thu, Apr 7, 2016 at 7:59 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Anyway, I think it would be reasonable to give this patch a few more
days in view of David's being away through the weekend. But the RMT
has final say on that.

The RMT has considered this request (sorry for the delay) and thought
it had merit, but ultimately voted not to give this patch an
extension.

Robert Haas
PostgreSQL 9.6 Release Management Team

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

#81David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#75)
Re: Performance improvement for joins where outer side is unique

On 8 April 2016 at 02:46, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I'm also a bit suspicious of the fact that some of the plans in
aggregates.out changed from merge to hash joins; with basically
no stats at hand in those tests, that seems dubious. A quick look
at what the patch touched in costsize.c suggests that this might
be because you've effectively allowed cost_hashjoin to give a cost
discount for inner unique, but provided no similar intelligence
in cost_mergejoin.

(catching up)

The only possible reason that the merge join plans have become a hash
join is that hash join is costed more cheaply (same as SEMI JOIN) when
the inner side is unique. The reason I didn't cost merge join
differently is that there seems to be no special costing done there
for SEMI joins already. I might be wrong, but I didn't feel like it
was up to this patch to introduce that, though, perhaps a patch should
go in before this one to do that.

I understand this is 9.7 stuff now, but I feel like I should tie up
the lose ends before they get forgotten.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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

#82David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#79)
Re: Performance improvement for joins where outer side is unique

On 8 April 2016 at 11:59, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I did some performance testing on the attached somewhat-cleaned-up patch,
and convinced myself that the planning time penalty is fairly minimal:
on the order of a couple percent in simple one-join queries, and less
than that in very large queries. Oddly, it seems that the result cacheing
done in get_optimal_jointype() is only barely worth the trouble in typical
cases; though if you get a query large enough to require GEQO, it's a win
because successive GEQO attempts can re-use cache entries made by earlier
attempts. (This may indicate something wrong with my testing procedure?
Seems like diking out the cache should have made more difference.)

It'll really depend on whether you're testing a positive case or a
negative one, since a GEQO join search will be the only time the
non-unique cache is filled, then if you're testing the negative case
then this is the only time you'll get any non-unique cache benefits.
On the other hand, if you were testing with a positive case then I
guess the unique index check is cheaper than we thought. Maybe adding
a couple of other unique indexes before defining the one which proves
the join unique (so that they've got a lower OID) would be enough to
start showing the cache benefits of the unique rel cache.

I did find by measurement that the negative-cache-entry code produces
exactly zero hits unless you're in GEQO mode, which is not really
surprising given the order in which the join search occurs. So in the
attached patch I made the code not bother with making negative cache
entries unless using GEQO, to hopefully save a few nanoseconds.

Yeah, I also noticed this in my testing, and it makes sense given how
the standard join search works. Thanks for making that improvement,
I've not yet looked at the code, but it sounds like a good idea.

I rebased over f338dd758, did a little bit of code cleanup and fixed some
bugs in the uniqueness detection logic, but have not reviewed the rest of
the patch since it's likely all gonna change if we reconsider the JoinType
representation.

Anyway, I think it would be reasonable to give this patch a few more
days in view of David's being away through the weekend. But the RMT
has final say on that.

Thanks for making the updates and committing the refactor of
analyzejoins.c. The RMT have ruled no extension will be given, so I'll
get this into shape for 9.7 CF1.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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

#83David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#76)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 8 April 2016 at 06:49, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:
Just had a thought about this, which should have crystallized a long
time ago perhaps. Where I'd originally imagined you were going with
this idea is to do what the thread title actually says, and check for
joins in which the *outer* side is unique. I can't see that that's
of any value for nestloop or hash joins, but for merge joins, knowing
that the outer side is unique could be extremely valuable because
we could skip doing mark/restore backups on the inner side, hugely
reducing the cost when the inner side has many duplicates. Now I'm
not asking for the initially-committed version of the patch to do that,
but I think we need to design it to be readily extensible to do so.

I've rebased the changes I made to address this back in April to current master.

The changes get rid of the changing JOIN_INNER to JOIN_SEMI conversion
and revert back to the original "inner_unique" marking of the joins.
In addition to this I've also added "outer_unique", which is only made
use of in merge join to control if the outer side needs to enable mark
and restore or not.

However, having said that, I'm not sure why we'd need outer_unique
available so we'd know that we could skip mark/restore. I think
inner_unique is enough for this purpose. Take the comment from
nodeMergejoin.c:

* outer: (0 ^1 1 2 5 5 5 6 6 7) current tuple: 1
* inner: (1 ^3 5 5 5 5 6) current tuple: 3
...
*
* Consider the above relations and suppose that the executor has
* just joined the first outer "5" with the last inner "5". The
* next step is of course to join the second outer "5" with all
* the inner "5's". This requires repositioning the inner "cursor"
* to point at the first inner "5". This is done by "marking" the
* first inner 5 so we can restore the "cursor" to it before joining
* with the second outer 5. The access method interface provides
* routines to mark and restore to a tuple.

If only one inner "5" can exist (unique_inner), then isn't the inner
side already in the correct place, and no restore is required?

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2016-10-31.patchapplication/octet-stream; name=unique_joins_2016-10-31.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 0a669d9..0b051d8 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1285,6 +1285,26 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+				ExplainPropertyText("Inner Unique", value, es);
+				value =  ((Join *) plan)->outer_unique ? "Yes" : "No";
+				ExplainPropertyText("Outer Unique", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 369e666..428c969 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need one inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_inner_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -453,6 +453,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -498,8 +506,11 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			hjstate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 6db09b8..8e52faa 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need one inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_inner_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1487,6 +1487,15 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner or outer side of
+	 * the join will at most contain a single tuple for the opposing side of
+	 * the join, then we can optimize the merge join by skipping to the next
+	 * tuple since we know there are no more to match.
+	 */
+	mergestate->js.match_first_inner_tuple_only = node->join.inner_unique;
+	mergestate->js.match_first_outer_tuple_only = node->join.outer_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1537,7 +1546,8 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	 * only if eflags doesn't specify REWIND.
 	 */
 	if (IsA(innerPlan(node), Material) &&
-		(eflags & EXEC_FLAG_REWIND) == 0)
+		(eflags & EXEC_FLAG_REWIND) == 0 &&
+		!node->join.outer_unique)
 		mergestate->mj_ExtraMarks = true;
 	else
 		mergestate->mj_ExtraMarks = false;
@@ -1553,8 +1563,11 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			mergestate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 555fa09..26f01ba 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need 1 inner tuple to
+			 * match.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.match_first_inner_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -311,6 +311,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -354,8 +362,11 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			nlstate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 71714bc..fd4448f 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2084,6 +2084,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(inner_unique);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 29a090f..1311062 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -852,6 +852,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(inner_unique);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index ae86954..b9cf93b 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2301,6 +2301,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(inner_unique);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index cc7384f..d572bf9 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -94,6 +95,17 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.
+	 */
+	extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+											jointype, sjinfo, restrictlist);
+
+	/* as above but with relations swapped */
+	extra.outer_unique = innerrel_is_unique(root, innerrel, outerrel,
+											jointype, sjinfo, restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -321,11 +333,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -383,11 +393,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -454,10 +462,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -519,11 +526,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -581,11 +586,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 74e4245..35c0941 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -34,6 +34,8 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+						   SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
@@ -41,6 +43,36 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
+
+
+/*
+ * mark_unique_joins
+ *		Check for proofs on each left joined relation which prove that the
+ *		join can't cause dupliction of RHS tuples.
+ */
+void
+mark_unique_joins(PlannerInfo *root)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs. If we can prove
+		 * these to have a unique inner side, based on the join condition then
+		 * this can save the executor from having to attempt fruitless
+		 * searches for subsequent matching outer tuples.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT && !sjinfo->inner_unique)
+			sjinfo->inner_unique = specialjoin_is_unique_join(root, sjinfo);
+	}
+}
 
 
 /*
@@ -95,6 +127,15 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * mark_unique_joins may have been previously unable to mark a join as
+		 * unique due to there being multiple relations on the RHS of the
+		 * join. We may have just removed a relation so that there's now just
+		 * a singleton relation, so let's try again to mark the joins as
+		 * unique.
+		 */
+		mark_unique_joins(root);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -156,31 +197,22 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	int			innerrelid;
 	RelOptInfo *innerrel;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must have a unique inner side and must be a non-delaying join to a
+	 * single baserel, else we aren't going to be able to do anything with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->inner_unique ||
 		sjinfo->delay_upper_joins)
 		return false;
 
 	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
-		return false;
+		return false;			/* should not happen */
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (!rel_supports_distinctness(root, innerrel))
-		return false;
-
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
 
@@ -190,7 +222,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -231,6 +264,41 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most one inner row for any single outer row. False is returned if
+ *		there's insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* if there's more than one relation involved, then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -238,9 +306,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 * it's what we want.  The mergejoinability test also eliminates clauses
 	 * containing volatile functions, which we couldn't depend on.
 	 */
-	foreach(l, innerrel->joininfo)
+	foreach(lc, innerrel->joininfo)
 	{
-		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(l);
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
 
 		/*
 		 * If it's not a join clause for this outer join, we can't use it.
@@ -252,10 +320,11 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we
+			 * can't say we're unique.  (XXX this is confusing conditions for
+			 * join removability with conditions for uniqueness, and could
+			 * probably stand to be improved.  But for the moment, keep on
+			 * applying the stricter condition.)
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -847,3 +916,168 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' can, at most, match a
+ *	  single tuple in 'outerrel' based on the join condition in
+ *	  'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist)
+{
+	int			innerrelid;
+	bool		unique = false;
+
+	/* left joins were already checked by mark_unique_joins */
+	if (jointype == JOIN_LEFT)
+		return sjinfo->inner_unique;
+
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/*
+	 * Any INNER JOINs which can be proven to return at most one inner tuple
+	 * for each outer tuple can be said to be unique.
+	 */
+	if (jointype == JOIN_INNER)
+	{
+		MemoryContext old_context;
+		ListCell   *lc;
+
+		/* can't prove uniqueness for joins with an empty restrictlist */
+		if (restrictlist == NIL)
+			return false;
+
+		/*
+		 * First let's query the unique and non-unique caches to see if we've
+		 * managed to prove that innerrel is unique for some subset of this
+		 * outerrel. We don't need an exact match, as if we have any extra
+		 * outerrels than were previously cached, then they can't make the
+		 * innerrel any less unique.
+		 */
+		foreach(lc, root->unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(unique_rels, outerrel->relids))
+				return true;		/* Success! */
+		}
+
+		/*
+		 * We may have previously determined that this outerrel, or some
+		 * superset thereof, cannot prove this innerrel to be unique.
+		 */
+		foreach(lc, root->non_unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(outerrel->relids, unique_rels))
+				return false;
+		}
+
+		/* No cached information, so try to make the proof. */
+		if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+		{
+			unique = true;		/* Success! */
+
+			/*
+			 * Cache the positive result for future probes, being sure to keep
+			 * it in the planner_cxt even if we are working in GEQO.
+			 *
+			 * Note: one might consider trying to isolate the minimal subset
+			 * of the outerrels that proved the innerrel unique.  But it's not
+			 * worth the trouble, because the planner builds up joinrels
+			 * incrementally and so we'll see the minimally sufficient
+			 * outerrels before any supersets of them anyway.
+			 */
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->unique_rels[innerrelid] =
+				lappend(root->unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+		else
+		{
+			/*
+			 * None of the join conditions for outerrel proved innerrel
+			 * unique, so we can safely reject this outerrel or any subset of
+			 * it in future checks.
+			 *
+			 * However, in normal planning mode, caching this knowledge is
+			 * totally pointless; it won't be queried again, because we build
+			 * up joinrels from smaller to larger.  It is useful in GEQO mode,
+			 * where the knowledge can be carried across successive planning
+			 * attempts; and it's likely to be useful when using join-search
+			 * plugins, too.  Hence cache only when join_search_private is
+			 * non-NULL.  (Yeah, that's a hack, but it seems reasonable.)
+			 */
+			if (root->join_search_private)
+			{
+				old_context = MemoryContextSwitchTo(root->planner_cxt);
+				root->non_unique_rels[innerrelid] =
+					lappend(root->non_unique_rels[innerrelid],
+							bms_copy(outerrel->relids));
+				MemoryContextSwitchTo(old_context);
+			}
+		}
+	}
+	return unique;
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ad49674..556376a 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -205,12 +205,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -225,7 +225,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3501,7 +3501,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -3804,7 +3804,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -3944,7 +3944,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5044,7 +5044,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5053,9 +5053,11 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
+	node->join.outer_unique = jpath->outer_unique;
 
 	return node;
 }
@@ -5067,7 +5069,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5077,8 +5079,10 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
+	node->join.outer_unique = jpath->outer_unique;
 
 	return node;
 }
@@ -5119,7 +5123,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5133,8 +5137,10 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
+	node->join.outer_unique = jpath->outer_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 84ce6b3..b779c28 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1130,6 +1130,8 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	/* this may be changed later */
+	sjinfo->inner_unique = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index e7ae7ae..73c8789 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
@@ -185,6 +188,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* check for unique properties on outer joins */
+	mark_unique_joins(root);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index abb7507..3e3c7e3 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1935,11 +1935,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -1950,16 +1948,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -1995,7 +1992,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2008,8 +2005,10 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
+	pathnode->outer_unique = extra->outer_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra->sjinfo, &extra->semifactors);
 
 	return pathnode;
 }
@@ -2022,10 +2021,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2038,10 +2036,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2049,6 +2046,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2058,7 +2056,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2071,12 +2069,14 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
+	pathnode->jpath.outer_unique = extra->outer_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra->sjinfo);
 
 	return pathnode;
 }
@@ -2088,12 +2088,10 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
  * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
- * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
  */
@@ -2102,15 +2100,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2120,7 +2117,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2145,10 +2142,12 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
+	pathnode->jpath.outer_unique = extra->outer_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra->sjinfo, &extra->semifactors);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index deef560..207dd02 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -79,6 +79,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f6f73f3..77953ee 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1641,6 +1641,17 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		match_first_inner_tuple_only;	/* True if we can safely move
+												 * to the next outer tuple
+												 * after matching first inner
+												 * tuple
+												 */
+	bool		match_first_outer_tuple_only;	/* True if we can safely move
+												 * to the next inner tuple
+												 * after matching first outer
+												 * tuple
+												 */
+
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index e2fbc7d..3b05b08 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -598,6 +598,8 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
+	bool		outer_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 3a1255a..e06adce 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -222,6 +222,19 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner or outer sides based on the join
+	 * condition. This is a rather expensive test to perform, as it requires
+	 * checking each relation's unique indexes to see if the relation can, at
+	 * most, return one tuple for each opposing tuple in the join. We use this
+	 * cache during the join search to record lists of the sets of relations
+	 * which both prove, and disprove the uniqueness properties for the relid
+	 * indexed by these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1208,6 +1221,11 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* inner side of join matches no more than one
+								 * outer side tuple */
+	bool		outer_unique;	/* outer side of join matches no more than one
+								 * inner side tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -1777,6 +1795,8 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		inner_unique;	/* inner side of join matches no more than one
+								 * outer side tuple */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -2009,6 +2029,10 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique marks if the inner side of join matches no more than one outer
+ * side tuple
+ * outer_unique marks if the outer side of join matches no more than one inner
+ * side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2017,6 +2041,8 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
+	bool		outer_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 71d9154..ca422d7 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -102,11 +102,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -114,10 +112,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -128,11 +125,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					  JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -234,6 +229,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 4fbb6cc..3f585c7 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -100,10 +100,15 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
-
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist);
 /*
  * prototypes for plan/setrefs.c
  */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 45208a6..218cc62 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,8 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
+         Outer Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +392,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(16 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index d9bbae0..233c796 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,8 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3371,14 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
+               Outer Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(18 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3416,13 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3430,13 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
+                           Outer Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3452,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(38 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3485,13 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3499,18 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
+                           Outer Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
+                                 Outer Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3527,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(44 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3561,13 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3575,18 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
+                           Outer Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
+                                 Outer Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3606,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(47 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3638,20 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
+               Outer Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
+                     Outer Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3661,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3691,13 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3709,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(20 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3734,17 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
+         Outer Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
+               Outer Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3760,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(28 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3786,16 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
+   Outer Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
+         Outer Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
+               Outer Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3805,8 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
+                     Outer Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3821,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(37 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3851,21 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
+   Outer Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
+               Outer Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
+                     Outer Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3882,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(33 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3970,8 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
+   Outer Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3984,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(16 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -4801,12 +4867,14 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(9 rows)
 
 select * from
   int8_tbl a left join
@@ -4833,12 +4901,14 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(9 rows)
 
 select * from
   int8_tbl a left join
@@ -4866,6 +4936,8 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4873,7 +4945,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(11 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4893,12 +4965,14 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(9 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4920,10 +4994,14 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4932,7 +5010,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(18 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4978,16 +5056,20 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
+         Outer Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(15 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5054,13 +5136,21 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
+   Outer Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
+               Outer Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5076,7 +5166,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(32 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5094,17 +5184,27 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
+               Outer Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
+                           Outer Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5126,7 +5226,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(44 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5139,16 +5239,20 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
+         Outer Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(15 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5161,10 +5265,14 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
+   Outer Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5173,7 +5281,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(18 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5200,10 +5308,14 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
+         Outer Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5225,7 +5337,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(31 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5323,3 +5435,255 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Outer Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(11 rows)
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(11 rows)
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Outer Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Unique
+         Output: j3.id
+         ->  Sort
+               Output: j3.id
+               Sort Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(14 rows)
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Group
+         Output: j3.id
+         Group Key: j3.id
+         ->  Sort
+               Output: j3.id
+               Sort Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(15 rows)
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Outer Unique: No
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of join conversions
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Outer Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(9 rows)
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index a2c36e4..ca5a4bd 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5389,12 +5389,14 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
+   Outer Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(9 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index f06cfa4..325815d 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2012,12 +2012,14 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(9 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2038,11 +2040,13 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index eda319d..dbcc09d 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -783,6 +783,8 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -791,7 +793,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(12 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -811,6 +813,8 @@ select * from int4_tbl o where (f1, f1) in
 ----------------------------------------------------------------
  Hash Semi Join
    Output: o.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -824,7 +828,7 @@ select * from int4_tbl o where (f1, f1) in
                      Group Key: i.f1
                      ->  Seq Scan on public.int4_tbl i
                            Output: i.f1
-(15 rows)
+(17 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index f60991e..77a2deb 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2136,6 +2136,8 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Inner Unique: No
+                     Outer Unique: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.ctid, t1_5.a, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 3) AND leakproof(t1_5.a))
@@ -2153,6 +2155,8 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.ctid, t11.a, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Inner Unique: No
+                     Outer Unique: No
                      ->  Seq Scan on public.t11
                            Output: t11.ctid, t11.a, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 3) AND leakproof(t11.a))
@@ -2170,6 +2174,8 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Inner Unique: No
+                     Outer Unique: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.ctid, t12_2.a, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 3) AND leakproof(t12_2.a))
@@ -2187,6 +2193,8 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Inner Unique: No
+                     Outer Unique: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.ctid, t111_3.a, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 3) AND leakproof(t111_3.a))
@@ -2197,7 +2205,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 3)
-(73 rows)
+(81 rows)
 
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a = 3;
 SELECT * FROM v1 WHERE a=100; -- Nothing should have been changed to 100
@@ -2226,6 +2234,8 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
                ->  Nested Loop Semi Join
                      Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c, t1_5.ctid, t12.ctid, t12.tableoid
+                     Inner Unique: No
+                     Outer Unique: No
                      ->  Seq Scan on public.t1 t1_5
                            Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c
                            Filter: ((t1_5.a > 5) AND (t1_5.a = 8) AND leakproof(t1_5.a))
@@ -2243,6 +2253,8 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
                ->  Nested Loop Semi Join
                      Output: t11.a, t11.ctid, t11.b, t11.c, t11.d, t11.ctid, t12_1.ctid, t12_1.tableoid
+                     Inner Unique: No
+                     Outer Unique: No
                      ->  Seq Scan on public.t11
                            Output: t11.a, t11.ctid, t11.b, t11.c, t11.d
                            Filter: ((t11.a > 5) AND (t11.a = 8) AND leakproof(t11.a))
@@ -2260,6 +2272,8 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
                ->  Nested Loop Semi Join
                      Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e, t12_2.ctid, t12_3.ctid, t12_3.tableoid
+                     Inner Unique: No
+                     Outer Unique: No
                      ->  Seq Scan on public.t12 t12_2
                            Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e
                            Filter: ((t12_2.a > 5) AND (t12_2.a = 8) AND leakproof(t12_2.a))
@@ -2277,6 +2291,8 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
                ->  Nested Loop Semi Join
                      Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e, t111_3.ctid, t12_4.ctid, t12_4.tableoid
+                     Inner Unique: No
+                     Outer Unique: No
                      ->  Seq Scan on public.t111 t111_3
                            Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e
                            Filter: ((t111_3.a > 5) AND (t111_3.a = 8) AND leakproof(t111_3.a))
@@ -2287,7 +2303,7 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                            ->  Seq Scan on public.t111 t111_4
                                  Output: t111_4.ctid, t111_4.tableoid, t111_4.a
                                  Filter: (t111_4.a = 8)
-(73 rows)
+(81 rows)
 
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
 NOTICE:  snooped value: 8
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 1b7f57b..06b8a13 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2183,6 +2183,8 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2190,6 +2192,8 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2197,6 +2201,8 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2204,12 +2210,14 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(46 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 97bccec..69e0109 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1730,3 +1730,95 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of join conversions
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#84David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#83)
Re: Performance improvement for joins where outer side is unique

On 31 October 2016 at 18:37, David Rowley <david.rowley@2ndquadrant.com> wrote:

I've rebased the changes I made to address this back in April to current master.

Please note that I went ahead and marked this as "Ready for
committer". It was previously marked as such in a previous commitfest.
The changes made since last version was based on feedback from Tom.

If anyone thinks this is not correct then please mark as "Ready for review".

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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

#85Haribabu Kommi
kommi.haribabu@gmail.com
In reply to: David Rowley (#84)
Re: Performance improvement for joins where outer side is unique

On Wed, Nov 2, 2016 at 1:21 PM, David Rowley <david.rowley@2ndquadrant.com>
wrote:

On 31 October 2016 at 18:37, David Rowley <david.rowley@2ndquadrant.com>
wrote:

I've rebased the changes I made to address this back in April to current

master.

Please note that I went ahead and marked this as "Ready for
committer". It was previously marked as such in a previous commitfest.
The changes made since last version was based on feedback from Tom.

If anyone thinks this is not correct then please mark as "Ready for
review".

Patch still applies fine to HEAD.
Moved to next CF with "ready for committer" status.

Regards,
Hari Babu
Fujitsu Australia

#86Robert Haas
robertmhaas@gmail.com
In reply to: Haribabu Kommi (#85)
Re: Performance improvement for joins where outer side is unique

On Dec 2, 2016, at 7:47 AM, Haribabu Kommi <kommi.haribabu@gmail.com> wrote:

On Wed, Nov 2, 2016 at 1:21 PM, David Rowley <david.rowley@2ndquadrant.com> wrote:
On 31 October 2016 at 18:37, David Rowley <david.rowley@2ndquadrant.com> wrote:

I've rebased the changes I made to address this back in April to current master.

Please note that I went ahead and marked this as "Ready for
committer". It was previously marked as such in a previous commitfest.
The changes made since last version was based on feedback from Tom.

If anyone thinks this is not correct then please mark as "Ready for review".

Patch still applies fine to HEAD.
Moved to next CF with "ready for committer" status.

Tom, are you picking this up?

...Robert

#87Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#86)
Re: Performance improvement for joins where outer side is unique

Robert Haas <robertmhaas@gmail.com> writes:

On Dec 2, 2016, at 7:47 AM, Haribabu Kommi <kommi.haribabu@gmail.com> wrote:

Patch still applies fine to HEAD.
Moved to next CF with "ready for committer" status.

Tom, are you picking this up?

Yeah, I apologize for not having gotten to it in this commitfest, but
it's definitely something I will look at.

regards, tom lane

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

#88David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#87)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 3 December 2016 at 10:26, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

On Dec 2, 2016, at 7:47 AM, Haribabu Kommi <kommi.haribabu@gmail.com> wrote:

Patch still applies fine to HEAD.
Moved to next CF with "ready for committer" status.

Tom, are you picking this up?

Yeah, I apologize for not having gotten to it in this commitfest, but
it's definitely something I will look at.

Old patch no longer applies, so I've attached a rebased patch. This
also re-adds a comment line which I mistakenly removed.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-01-19.patchapplication/octet-stream; name=unique_joins_2017-01-19.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index ee7046c..d07feb2 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1309,6 +1309,26 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+				ExplainPropertyText("Inner Unique", value, es);
+				value =  ((Join *) plan)->outer_unique ? "Yes" : "No";
+				ExplainPropertyText("Outer Unique", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index b41e4e2..5b75b64 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need one inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_inner_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -453,6 +453,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -498,8 +506,11 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			hjstate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 2fd1856..378706f 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need one inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_inner_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1487,6 +1487,15 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner or outer side of
+	 * the join will at most contain a single tuple for the opposing side of
+	 * the join, then we can optimize the merge join by skipping to the next
+	 * tuple since we know there are no more to match.
+	 */
+	mergestate->js.match_first_inner_tuple_only = node->join.inner_unique;
+	mergestate->js.match_first_outer_tuple_only = node->join.outer_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1537,7 +1546,8 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	 * only if eflags doesn't specify REWIND.
 	 */
 	if (IsA(innerPlan(node), Material) &&
-		(eflags & EXEC_FLAG_REWIND) == 0)
+		(eflags & EXEC_FLAG_REWIND) == 0 &&
+		!node->join.outer_unique)
 		mergestate->mj_ExtraMarks = true;
 	else
 		mergestate->mj_ExtraMarks = false;
@@ -1553,8 +1563,11 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			mergestate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e058427..4345e72 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need 1 inner tuple to
+			 * match.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.match_first_inner_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -311,6 +311,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -354,8 +362,11 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			nlstate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 9f6a7e6..d2f3aa1 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2088,6 +2088,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(inner_unique);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 78ed3c7..efc3e43 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -853,6 +853,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(inner_unique);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index c2ba38e..663a930 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2309,6 +2309,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(inner_unique);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 7c30ec6..49a1fb4 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -103,6 +104,17 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.
+	 */
+	extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+											jointype, sjinfo, restrictlist);
+
+	/* as above but with relations swapped */
+	extra.outer_unique = innerrel_is_unique(root, innerrel, outerrel,
+											jointype, sjinfo, restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -330,11 +342,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -392,11 +402,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -463,10 +471,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -528,11 +535,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -590,11 +595,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 438baf1..81a0a75 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -34,6 +34,8 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+						   SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
@@ -41,6 +43,36 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
+
+
+/*
+ * mark_unique_joins
+ *		Check for proofs on each left joined relation which prove that the
+ *		join can't cause dupliction of RHS tuples.
+ */
+void
+mark_unique_joins(PlannerInfo *root)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs. If we can prove
+		 * these to have a unique inner side, based on the join condition then
+		 * this can save the executor from having to attempt fruitless
+		 * searches for subsequent matching outer tuples.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT && !sjinfo->inner_unique)
+			sjinfo->inner_unique = specialjoin_is_unique_join(root, sjinfo);
+	}
+}
 
 
 /*
@@ -95,6 +127,15 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * mark_unique_joins may have been previously unable to mark a join as
+		 * unique due to there being multiple relations on the RHS of the
+		 * join. We may have just removed a relation so that there's now just
+		 * a singleton relation, so let's try again to mark the joins as
+		 * unique.
+		 */
+		mark_unique_joins(root);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -156,31 +197,22 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	int			innerrelid;
 	RelOptInfo *innerrel;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must have a unique inner side and must be a non-delaying join to a
+	 * single baserel, else we aren't going to be able to do anything with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->inner_unique ||
 		sjinfo->delay_upper_joins)
 		return false;
 
 	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
-		return false;
+		return false;			/* should not happen */
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (!rel_supports_distinctness(root, innerrel))
-		return false;
-
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
 
@@ -190,7 +222,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -231,6 +264,41 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most one inner row for any single outer row. False is returned if
+ *		there's insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* if there's more than one relation involved, then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -238,9 +306,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 * it's what we want.  The mergejoinability test also eliminates clauses
 	 * containing volatile functions, which we couldn't depend on.
 	 */
-	foreach(l, innerrel->joininfo)
+	foreach(lc, innerrel->joininfo)
 	{
-		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(l);
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
 
 		/*
 		 * If it's not a join clause for this outer join, we can't use it.
@@ -252,10 +320,11 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we
+			 * can't say we're unique.  (XXX this is confusing conditions for
+			 * join removability with conditions for uniqueness, and could
+			 * probably stand to be improved.  But for the moment, keep on
+			 * applying the stricter condition.)
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -847,3 +916,168 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' can, at most, match a
+ *	  single tuple in 'outerrel' based on the join condition in
+ *	  'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist)
+{
+	int			innerrelid;
+	bool		unique = false;
+
+	/* left joins were already checked by mark_unique_joins */
+	if (jointype == JOIN_LEFT)
+		return sjinfo->inner_unique;
+
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/*
+	 * Any INNER JOINs which can be proven to return at most one inner tuple
+	 * for each outer tuple can be said to be unique.
+	 */
+	if (jointype == JOIN_INNER)
+	{
+		MemoryContext old_context;
+		ListCell   *lc;
+
+		/* can't prove uniqueness for joins with an empty restrictlist */
+		if (restrictlist == NIL)
+			return false;
+
+		/*
+		 * First let's query the unique and non-unique caches to see if we've
+		 * managed to prove that innerrel is unique for some subset of this
+		 * outerrel. We don't need an exact match, as if we have any extra
+		 * outerrels than were previously cached, then they can't make the
+		 * innerrel any less unique.
+		 */
+		foreach(lc, root->unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(unique_rels, outerrel->relids))
+				return true;		/* Success! */
+		}
+
+		/*
+		 * We may have previously determined that this outerrel, or some
+		 * superset thereof, cannot prove this innerrel to be unique.
+		 */
+		foreach(lc, root->non_unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(outerrel->relids, unique_rels))
+				return false;
+		}
+
+		/* No cached information, so try to make the proof. */
+		if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+		{
+			unique = true;		/* Success! */
+
+			/*
+			 * Cache the positive result for future probes, being sure to keep
+			 * it in the planner_cxt even if we are working in GEQO.
+			 *
+			 * Note: one might consider trying to isolate the minimal subset
+			 * of the outerrels that proved the innerrel unique.  But it's not
+			 * worth the trouble, because the planner builds up joinrels
+			 * incrementally and so we'll see the minimally sufficient
+			 * outerrels before any supersets of them anyway.
+			 */
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->unique_rels[innerrelid] =
+				lappend(root->unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+		else
+		{
+			/*
+			 * None of the join conditions for outerrel proved innerrel
+			 * unique, so we can safely reject this outerrel or any subset of
+			 * it in future checks.
+			 *
+			 * However, in normal planning mode, caching this knowledge is
+			 * totally pointless; it won't be queried again, because we build
+			 * up joinrels from smaller to larger.  It is useful in GEQO mode,
+			 * where the knowledge can be carried across successive planning
+			 * attempts; and it's likely to be useful when using join-search
+			 * plugins, too.  Hence cache only when join_search_private is
+			 * non-NULL.  (Yeah, that's a hack, but it seems reasonable.)
+			 */
+			if (root->join_search_private)
+			{
+				old_context = MemoryContextSwitchTo(root->planner_cxt);
+				root->non_unique_rels[innerrelid] =
+					lappend(root->non_unique_rels[innerrelid],
+							bms_copy(outerrel->relids));
+				MemoryContextSwitchTo(old_context);
+			}
+		}
+	}
+	return unique;
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c4ada21..e0060a9 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -205,12 +205,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -225,7 +225,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3501,7 +3501,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -3804,7 +3804,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -3944,7 +3944,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5081,7 +5081,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5090,9 +5090,11 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
+	node->join.outer_unique = jpath->outer_unique;
 
 	return node;
 }
@@ -5104,7 +5106,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5114,8 +5116,10 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
+	node->join.outer_unique = jpath->outer_unique;
 
 	return node;
 }
@@ -5156,7 +5160,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5170,8 +5174,10 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
+	node->join.outer_unique = jpath->outer_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index c170e96..5993d0b 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1204,6 +1204,8 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	/* this may be changed later */
+	sjinfo->inner_unique = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index e880759..46dbe76 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
@@ -185,6 +188,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* check for unique properties on outer joins */
+	mark_unique_joins(root);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 3b7c56d..928fa50 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1935,11 +1935,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -1950,16 +1948,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -1995,7 +1992,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2008,8 +2005,10 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
+	pathnode->outer_unique = extra->outer_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra->sjinfo, &extra->semifactors);
 
 	return pathnode;
 }
@@ -2022,10 +2021,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2038,10 +2036,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2049,6 +2046,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2058,7 +2056,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2071,12 +2069,14 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
+	pathnode->jpath.outer_unique = extra->outer_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra->sjinfo);
 
 	return pathnode;
 }
@@ -2088,11 +2088,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2102,15 +2100,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2120,7 +2117,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2145,10 +2142,12 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
+	pathnode->jpath.outer_unique = extra->outer_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra->sjinfo, &extra->semifactors);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index adc1db9..d1bda16 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -81,6 +81,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index ce13bf7..5b249c9 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1662,6 +1662,17 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		match_first_inner_tuple_only;	/* True if we can safely move
+												 * to the next outer tuple
+												 * after matching first inner
+												 * tuple
+												 */
+	bool		match_first_outer_tuple_only;	/* True if we can safely move
+												 * to the next inner tuple
+												 * after matching first outer
+												 * tuple
+												 */
+
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 6810f8c..3a93a38 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -607,6 +607,8 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
+	bool		outer_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 1e950c4..da731fc 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -222,6 +222,19 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner or outer sides based on the join
+	 * condition. This is a rather expensive test to perform, as it requires
+	 * checking each relation's unique indexes to see if the relation can, at
+	 * most, return one tuple for each opposing tuple in the join. We use this
+	 * cache during the join search to record lists of the sets of relations
+	 * which both prove, and disprove the uniqueness properties for the relid
+	 * indexed by these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1217,6 +1230,11 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* inner side of join matches no more than one
+								 * outer side tuple */
+	bool		outer_unique;	/* outer side of join matches no more than one
+								 * inner side tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -1799,6 +1817,8 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		inner_unique;	/* inner side of join matches no more than one
+								 * outer side tuple */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -2031,6 +2051,10 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique marks if the inner side of join matches no more than one outer
+ * side tuple
+ * outer_unique marks if the outer side of join matches no more than one inner
+ * side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2039,6 +2063,8 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
+	bool		outer_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index d16f879..b49f988 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -102,11 +102,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -114,10 +112,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -128,11 +125,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					  JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -234,6 +229,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..128753a 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -102,10 +102,15 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
-
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist);
 /*
  * prototypes for plan/setrefs.c
  */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index fa1f5e7..b623b7b 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,8 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
+         Outer Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +392,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(16 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index d9bbae0..233c796 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,8 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3371,14 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
+               Outer Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(18 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3416,13 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3430,13 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
+                           Outer Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3452,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(38 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3485,13 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3499,18 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
+                           Outer Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
+                                 Outer Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3527,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(44 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3561,13 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3575,18 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
+                           Outer Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
+                                 Outer Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3606,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(47 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3638,20 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
+               Outer Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
+                     Outer Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3661,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3691,13 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3709,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(20 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3734,17 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
+         Outer Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
+               Outer Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3760,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(28 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3786,16 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
+   Outer Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
+         Outer Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
+               Outer Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3805,8 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
+                     Outer Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3821,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(37 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3851,21 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
+   Outer Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
+               Outer Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
+                     Outer Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3882,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(33 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3970,8 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
+   Outer Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3984,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(16 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -4801,12 +4867,14 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(9 rows)
 
 select * from
   int8_tbl a left join
@@ -4833,12 +4901,14 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(9 rows)
 
 select * from
   int8_tbl a left join
@@ -4866,6 +4936,8 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4873,7 +4945,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(11 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4893,12 +4965,14 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(9 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4920,10 +4994,14 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4932,7 +5010,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(18 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4978,16 +5056,20 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
+         Outer Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(15 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5054,13 +5136,21 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
+   Outer Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
+               Outer Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5076,7 +5166,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(32 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5094,17 +5184,27 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
+               Outer Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
+                           Outer Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5126,7 +5226,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(44 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5139,16 +5239,20 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
+         Outer Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(15 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5161,10 +5265,14 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
+   Outer Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5173,7 +5281,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(18 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5200,10 +5308,14 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
+         Outer Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5225,7 +5337,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(31 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5323,3 +5435,255 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Outer Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(11 rows)
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(11 rows)
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Outer Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Unique
+         Output: j3.id
+         ->  Sort
+               Output: j3.id
+               Sort Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(14 rows)
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Group
+         Output: j3.id
+         Group Key: j3.id
+         ->  Sort
+               Output: j3.id
+               Sort Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(15 rows)
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Outer Unique: No
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of join conversions
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Outer Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(9 rows)
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 79513e4..7e948ed 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,14 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
+   Outer Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(9 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 275b662..81f633b 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2012,12 +2012,14 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(9 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2038,11 +2040,13 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index eda319d..dbcc09d 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -783,6 +783,8 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -791,7 +793,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(12 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -811,6 +813,8 @@ select * from int4_tbl o where (f1, f1) in
 ----------------------------------------------------------------
  Hash Semi Join
    Output: o.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -824,7 +828,7 @@ select * from int4_tbl o where (f1, f1) in
                      Group Key: i.f1
                      ->  Seq Scan on public.int4_tbl i
                            Output: i.f1
-(15 rows)
+(17 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 02fa08e..aa012d4 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2183,6 +2183,8 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2190,6 +2192,8 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2197,6 +2201,8 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2204,12 +2210,14 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(46 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 97bccec..69e0109 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1730,3 +1730,95 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of join conversions
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#89David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#88)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 19 January 2017 at 11:06, David Rowley <david.rowley@2ndquadrant.com> wrote:

Old patch no longer applies, so I've attached a rebased patch. This
also re-adds a comment line which I mistakenly removed.

(meanwhile Andres commits 69f4b9c)

I should've waited a bit longer.

Here's another that fixes the new conflicts.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-01-19a.patchtext/x-patch; charset=US-ASCII; name=unique_joins_2017-01-19a.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index f9fb276..239f78b 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1312,6 +1312,26 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+				ExplainPropertyText("Inner Unique", value, es);
+				value =  ((Join *) plan)->outer_unique ? "Yes" : "No";
+				ExplainPropertyText("Outer Unique", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index b41e4e2..5b75b64 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -306,10 +306,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need one inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_inner_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -453,6 +453,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -498,8 +506,11 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			hjstate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 2fd1856..378706f 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -840,10 +840,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need one inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_inner_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1487,6 +1487,15 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner or outer side of
+	 * the join will at most contain a single tuple for the opposing side of
+	 * the join, then we can optimize the merge join by skipping to the next
+	 * tuple since we know there are no more to match.
+	 */
+	mergestate->js.match_first_inner_tuple_only = node->join.inner_unique;
+	mergestate->js.match_first_outer_tuple_only = node->join.outer_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1537,7 +1546,8 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	 * only if eflags doesn't specify REWIND.
 	 */
 	if (IsA(innerPlan(node), Material) &&
-		(eflags & EXEC_FLAG_REWIND) == 0)
+		(eflags & EXEC_FLAG_REWIND) == 0 &&
+		!node->join.outer_unique)
 		mergestate->mj_ExtraMarks = true;
 	else
 		mergestate->mj_ExtraMarks = false;
@@ -1553,8 +1563,11 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			mergestate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index e058427..4345e72 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -247,10 +247,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need 1 inner tuple to
+			 * match.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.match_first_inner_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -311,6 +311,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -354,8 +362,11 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			nlstate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index f871e9d..b85b51c 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2104,6 +2104,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(inner_unique);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 78ed3c7..efc3e43 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -853,6 +853,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(inner_unique);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 1560ac3..8dd0633 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2327,6 +2327,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(inner_unique);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 7c30ec6..49a1fb4 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -103,6 +104,17 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.
+	 */
+	extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+											jointype, sjinfo, restrictlist);
+
+	/* as above but with relations swapped */
+	extra.outer_unique = innerrel_is_unique(root, innerrel, outerrel,
+											jointype, sjinfo, restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -330,11 +342,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -392,11 +402,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -463,10 +471,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -528,11 +535,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -590,11 +595,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 438baf1..81a0a75 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -34,6 +34,8 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+						   SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
@@ -41,6 +43,36 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
+
+
+/*
+ * mark_unique_joins
+ *		Check for proofs on each left joined relation which prove that the
+ *		join can't cause dupliction of RHS tuples.
+ */
+void
+mark_unique_joins(PlannerInfo *root)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs. If we can prove
+		 * these to have a unique inner side, based on the join condition then
+		 * this can save the executor from having to attempt fruitless
+		 * searches for subsequent matching outer tuples.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT && !sjinfo->inner_unique)
+			sjinfo->inner_unique = specialjoin_is_unique_join(root, sjinfo);
+	}
+}
 
 
 /*
@@ -95,6 +127,15 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * mark_unique_joins may have been previously unable to mark a join as
+		 * unique due to there being multiple relations on the RHS of the
+		 * join. We may have just removed a relation so that there's now just
+		 * a singleton relation, so let's try again to mark the joins as
+		 * unique.
+		 */
+		mark_unique_joins(root);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -156,31 +197,22 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	int			innerrelid;
 	RelOptInfo *innerrel;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must have a unique inner side and must be a non-delaying join to a
+	 * single baserel, else we aren't going to be able to do anything with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->inner_unique ||
 		sjinfo->delay_upper_joins)
 		return false;
 
 	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
-		return false;
+		return false;			/* should not happen */
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (!rel_supports_distinctness(root, innerrel))
-		return false;
-
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
 
@@ -190,7 +222,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -231,6 +264,41 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most one inner row for any single outer row. False is returned if
+ *		there's insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* if there's more than one relation involved, then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -238,9 +306,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 * it's what we want.  The mergejoinability test also eliminates clauses
 	 * containing volatile functions, which we couldn't depend on.
 	 */
-	foreach(l, innerrel->joininfo)
+	foreach(lc, innerrel->joininfo)
 	{
-		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(l);
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
 
 		/*
 		 * If it's not a join clause for this outer join, we can't use it.
@@ -252,10 +320,11 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we
+			 * can't say we're unique.  (XXX this is confusing conditions for
+			 * join removability with conditions for uniqueness, and could
+			 * probably stand to be improved.  But for the moment, keep on
+			 * applying the stricter condition.)
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -847,3 +916,168 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' can, at most, match a
+ *	  single tuple in 'outerrel' based on the join condition in
+ *	  'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist)
+{
+	int			innerrelid;
+	bool		unique = false;
+
+	/* left joins were already checked by mark_unique_joins */
+	if (jointype == JOIN_LEFT)
+		return sjinfo->inner_unique;
+
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/*
+	 * Any INNER JOINs which can be proven to return at most one inner tuple
+	 * for each outer tuple can be said to be unique.
+	 */
+	if (jointype == JOIN_INNER)
+	{
+		MemoryContext old_context;
+		ListCell   *lc;
+
+		/* can't prove uniqueness for joins with an empty restrictlist */
+		if (restrictlist == NIL)
+			return false;
+
+		/*
+		 * First let's query the unique and non-unique caches to see if we've
+		 * managed to prove that innerrel is unique for some subset of this
+		 * outerrel. We don't need an exact match, as if we have any extra
+		 * outerrels than were previously cached, then they can't make the
+		 * innerrel any less unique.
+		 */
+		foreach(lc, root->unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(unique_rels, outerrel->relids))
+				return true;		/* Success! */
+		}
+
+		/*
+		 * We may have previously determined that this outerrel, or some
+		 * superset thereof, cannot prove this innerrel to be unique.
+		 */
+		foreach(lc, root->non_unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(outerrel->relids, unique_rels))
+				return false;
+		}
+
+		/* No cached information, so try to make the proof. */
+		if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+		{
+			unique = true;		/* Success! */
+
+			/*
+			 * Cache the positive result for future probes, being sure to keep
+			 * it in the planner_cxt even if we are working in GEQO.
+			 *
+			 * Note: one might consider trying to isolate the minimal subset
+			 * of the outerrels that proved the innerrel unique.  But it's not
+			 * worth the trouble, because the planner builds up joinrels
+			 * incrementally and so we'll see the minimally sufficient
+			 * outerrels before any supersets of them anyway.
+			 */
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->unique_rels[innerrelid] =
+				lappend(root->unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+		else
+		{
+			/*
+			 * None of the join conditions for outerrel proved innerrel
+			 * unique, so we can safely reject this outerrel or any subset of
+			 * it in future checks.
+			 *
+			 * However, in normal planning mode, caching this knowledge is
+			 * totally pointless; it won't be queried again, because we build
+			 * up joinrels from smaller to larger.  It is useful in GEQO mode,
+			 * where the knowledge can be carried across successive planning
+			 * attempts; and it's likely to be useful when using join-search
+			 * plugins, too.  Hence cache only when join_search_private is
+			 * non-NULL.  (Yeah, that's a hack, but it seems reasonable.)
+			 */
+			if (root->join_search_private)
+			{
+				old_context = MemoryContextSwitchTo(root->planner_cxt);
+				root->non_unique_rels[innerrelid] =
+					lappend(root->non_unique_rels[innerrelid],
+							bms_copy(outerrel->relids));
+				MemoryContextSwitchTo(old_context);
+			}
+		}
+	}
+	return unique;
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index fae1f67..8904697 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -206,12 +206,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -226,7 +226,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3532,7 +3532,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -3835,7 +3835,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -3975,7 +3975,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5112,7 +5112,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5121,9 +5121,11 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
+	node->join.outer_unique = jpath->outer_unique;
 
 	return node;
 }
@@ -5135,7 +5137,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5145,8 +5147,10 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
+	node->join.outer_unique = jpath->outer_unique;
 
 	return node;
 }
@@ -5187,7 +5191,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5201,8 +5205,10 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
+	node->join.outer_unique = jpath->outer_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index c170e96..5993d0b 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1204,6 +1204,8 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	/* this may be changed later */
+	sjinfo->inner_unique = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index e880759..46dbe76 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
@@ -185,6 +188,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* check for unique properties on outer joins */
+	mark_unique_joins(root);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index f440875..6ebbcff 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1935,11 +1935,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -1950,16 +1948,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -1995,7 +1992,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2008,8 +2005,10 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
+	pathnode->outer_unique = extra->outer_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra->sjinfo, &extra->semifactors);
 
 	return pathnode;
 }
@@ -2022,10 +2021,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2038,10 +2036,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2049,6 +2046,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2058,7 +2056,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2071,12 +2069,14 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
+	pathnode->jpath.outer_unique = extra->outer_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra->sjinfo);
 
 	return pathnode;
 }
@@ -2088,11 +2088,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2102,15 +2100,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2120,7 +2117,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2145,10 +2142,12 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
+	pathnode->jpath.outer_unique = extra->outer_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra->sjinfo, &extra->semifactors);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index adc1db9..d1bda16 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -81,6 +81,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 1da1e1f..8f46e72 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1680,6 +1680,17 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		match_first_inner_tuple_only;	/* True if we can safely move
+												 * to the next outer tuple
+												 * after matching first inner
+												 * tuple
+												 */
+	bool		match_first_outer_tuple_only;	/* True if we can safely move
+												 * to the next inner tuple
+												 * after matching first outer
+												 * tuple
+												 */
+
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index f72f7a8..ce2285e 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -618,6 +618,8 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
+	bool		outer_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 643be54..845af01 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -222,6 +222,19 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner or outer sides based on the join
+	 * condition. This is a rather expensive test to perform, as it requires
+	 * checking each relation's unique indexes to see if the relation can, at
+	 * most, return one tuple for each opposing tuple in the join. We use this
+	 * cache during the join search to record lists of the sets of relations
+	 * which both prove, and disprove the uniqueness properties for the relid
+	 * indexed by these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1217,6 +1230,11 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* inner side of join matches no more than one
+								 * outer side tuple */
+	bool		outer_unique;	/* outer side of join matches no more than one
+								 * inner side tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -1810,6 +1828,8 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		inner_unique;	/* inner side of join matches no more than one
+								 * outer side tuple */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -2042,6 +2062,10 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique marks if the inner side of join matches no more than one outer
+ * side tuple
+ * outer_unique marks if the outer side of join matches no more than one inner
+ * side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2050,6 +2074,8 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
+	bool		outer_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 7b41317..5ce410c 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -102,11 +102,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -114,10 +112,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -128,11 +125,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					  JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -238,6 +233,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..128753a 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -102,10 +102,15 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
-
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist);
 /*
  * prototypes for plan/setrefs.c
  */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..7604d20 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,8 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
+         Outer Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +392,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(16 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index d9bbae0..233c796 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,8 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3371,14 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
+               Outer Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(18 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3416,13 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3430,13 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
+                           Outer Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3452,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(38 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3485,13 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3499,18 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
+                           Outer Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
+                                 Outer Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3527,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(44 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3561,13 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3575,18 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
+                           Outer Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
+                                 Outer Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3606,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(47 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3638,20 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
+               Outer Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
+                     Outer Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3661,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3691,13 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3709,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(20 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3734,17 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
+         Outer Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
+               Outer Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3760,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(28 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3786,16 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
+   Outer Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
+         Outer Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
+               Outer Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3805,8 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
+                     Outer Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3821,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(37 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3851,21 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
+   Outer Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
+               Outer Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
+                     Outer Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3882,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(33 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3970,8 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
+   Outer Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3984,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(16 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -4801,12 +4867,14 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(9 rows)
 
 select * from
   int8_tbl a left join
@@ -4833,12 +4901,14 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(9 rows)
 
 select * from
   int8_tbl a left join
@@ -4866,6 +4936,8 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4873,7 +4945,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(11 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4893,12 +4965,14 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(9 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4920,10 +4994,14 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4932,7 +5010,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(18 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4978,16 +5056,20 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
+         Outer Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(15 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5054,13 +5136,21 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
+   Outer Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
+               Outer Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5076,7 +5166,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(32 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5094,17 +5184,27 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
+               Outer Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
+                           Outer Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5126,7 +5226,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(44 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5139,16 +5239,20 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
+         Outer Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(15 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5161,10 +5265,14 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
+   Outer Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5173,7 +5281,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(18 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5200,10 +5308,14 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
+         Outer Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5225,7 +5337,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(31 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5323,3 +5435,255 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Outer Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(11 rows)
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(11 rows)
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Outer Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Unique
+         Output: j3.id
+         ->  Sort
+               Output: j3.id
+               Sort Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(14 rows)
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Group
+         Output: j3.id
+         Group Key: j3.id
+         ->  Sort
+               Output: j3.id
+               Sort Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(15 rows)
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Outer Unique: No
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of join conversions
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Outer Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(9 rows)
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 79513e4..7e948ed 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,14 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
+   Outer Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(9 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 56481de..434de62 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2010,12 +2010,14 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(9 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2036,11 +2038,13 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index abd3217..e95c42b 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -783,6 +783,8 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -791,7 +793,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(12 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -811,6 +813,8 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -828,7 +832,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(21 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 02fa08e..aa012d4 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2183,6 +2183,8 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2190,6 +2192,8 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2197,6 +2201,8 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2204,12 +2210,14 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(46 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 97bccec..69e0109 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1730,3 +1730,95 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of join conversions
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#90Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#83)
Re: Performance improvement for joins where outer side is unique

[ getting back to this at long last ]

David Rowley <david.rowley@2ndquadrant.com> writes:

However, having said that, I'm not sure why we'd need outer_unique
available so we'd know that we could skip mark/restore. I think
inner_unique is enough for this purpose. Take the comment from
nodeMergejoin.c:

* outer: (0 ^1 1 2 5 5 5 6 6 7) current tuple: 1
* inner: (1 ^3 5 5 5 5 6) current tuple: 3
...
*
* Consider the above relations and suppose that the executor has
* just joined the first outer "5" with the last inner "5". The
* next step is of course to join the second outer "5" with all
* the inner "5's". This requires repositioning the inner "cursor"
* to point at the first inner "5". This is done by "marking" the
* first inner 5 so we can restore the "cursor" to it before joining
* with the second outer 5. The access method interface provides
* routines to mark and restore to a tuple.

If only one inner "5" can exist (unique_inner), then isn't the inner
side already in the correct place, and no restore is required?

Hmm ... let me see whether I have my head wrapped around this correctly.

What I had in mind is a different optimization. When the inner is not
unique, we can't know we're done joining the first outer "5" until we
reach the inner "6". At that point, what we normally do is advance the
outer by one row and back up the inner to the mark point (the first inner
"5"). But the only reason to back up is that the new outer row might join
to the same inner rows the previous one did. If we know the outer side is
unique, then those inner rows can't join to the new outer row so no need
to back up. So this requires no real change to the mergejoin algorithm,
we just skip the mark and restore steps.

I think what you're saying is that, if we know the inner side is unique,
we can also skip mark/restore overhead; but it would work a bit
differently. After joining two rows with equal keys, instead of advancing
the inner as per standard algorithm, we'd need to advance the outer side.
(This is OK because we know the next inner can't join to this same outer.
But we don't know if the next outer can join to this inner.) We advance
inner only when current inner < current outer, so we're done with that
inner and need never rewind. So this is a more fundamental algorithm
change but it gets the same performance benefit.

So the question is, if we can skip mark/restore overhead when we know that
either input is unique, is it necessary for the planner to account for
both ways explicitly? Given the general symmetry of mergejoins, it might
be okay for the planner to preferentially generate plans with the unique
input on the inside, and not worry about optimizing in this way when it's
on the outside.

Now, JOIN_SEMI and JOIN_ANTI cases are *not* symmetric, since we don't
implement reverse-semi or reverse-anti join modes. But I don't know that
there's anything to be won by noticing that the outer side is unique in
those cases. I think probably we can apply the same technique of
advancing outer not inner, and never rewinding, in those join modes
whether or not either input is known unique.

Another asymmetry is that if one input is likely to be empty, it's better
to put that one on the outside, because if it is empty we don't have to
fetch anything from the other input (thus saving its startup cost). But
the planner doesn't account for that anyway because it never believes
non-dummy relations are truly empty; so even if it's getting this right
today, it's purely by chance.

I can't think of any other asymmetries offhand.

In short, I think you are right that it might be enough to account for
inner uniqueness only, and not worry about it for the outer side, even
for mergejoin. This means my previous objection is wrong and we don't
really have to allow for a future extension of that kind while choosing
the notation the planner uses.

So ... would you rather go back to the previous notation (extra
JoinTypes), or do you like the separate boolean better anyway?

Sorry for jerking you back and forth like this, but sometimes the
correct path isn't apparent from the start.

regards, tom lane

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

#91Antonin Houska
ah@cybertec.at
In reply to: David Rowley (#89)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> wrote:

On 19 January 2017 at 11:06, David Rowley <david.rowley@2ndquadrant.com> wrote:

Old patch no longer applies, so I've attached a rebased patch. This
also re-adds a comment line which I mistakenly removed.

(meanwhile Andres commits 69f4b9c)

I should've waited a bit longer.

Here's another that fixes the new conflicts.

I suspect that "inner" and "outer" relation / tuple are sometimes confused in
comments:

* analyzejoins.c:70

"searches for subsequent matching outer tuples."

* analyzejoins.c:972

/*
* innerrel_is_unique
* Check for proofs which prove that 'innerrel' can, at most, match a
* single tuple in 'outerrel' based on the join condition in
* 'restrictlist'.
*/

* relation.h:1831

bool inner_unique; /* inner side of join matches no more than one
* outer side tuple */

--
Antonin Houska
Cybertec Schönig & Schönig GmbH
Gröhrmühlgasse 26
A-2700 Wiener Neustadt
Web: http://www.postgresql-support.de, http://www.cybertec.at

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

#92David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#90)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

Thank for looking at this again.

On 25 January 2017 at 06:27, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

However, having said that, I'm not sure why we'd need outer_unique
available so we'd know that we could skip mark/restore. I think
inner_unique is enough for this purpose. Take the comment from
nodeMergejoin.c:

* outer: (0 ^1 1 2 5 5 5 6 6 7) current tuple: 1
* inner: (1 ^3 5 5 5 5 6) current tuple: 3
...
*
* Consider the above relations and suppose that the executor has
* just joined the first outer "5" with the last inner "5". The
* next step is of course to join the second outer "5" with all
* the inner "5's". This requires repositioning the inner "cursor"
* to point at the first inner "5". This is done by "marking" the
* first inner 5 so we can restore the "cursor" to it before joining
* with the second outer 5. The access method interface provides
* routines to mark and restore to a tuple.

If only one inner "5" can exist (unique_inner), then isn't the inner
side already in the correct place, and no restore is required?

Hmm ... let me see whether I have my head wrapped around this correctly.

What I had in mind is a different optimization. When the inner is not
unique, we can't know we're done joining the first outer "5" until we
reach the inner "6". At that point, what we normally do is advance the
outer by one row and back up the inner to the mark point (the first inner
"5"). But the only reason to back up is that the new outer row might join
to the same inner rows the previous one did. If we know the outer side is
unique, then those inner rows can't join to the new outer row so no need
to back up. So this requires no real change to the mergejoin algorithm,
we just skip the mark and restore steps.

I'd not thought of that possibility, although with a unique outer, I
don't think it'll ever save a restore, as the comparison to the new
outer tuple will always fail to match the marked inner tuple.
Never-the-less we can save that useless comparison, so I've written
code to that affect in the attached. Perhaps detecting unique outer is
not worthwhile for just this?

I think what you're saying is that, if we know the inner side is unique,
we can also skip mark/restore overhead; but it would work a bit
differently. After joining two rows with equal keys, instead of advancing
the inner as per standard algorithm, we'd need to advance the outer side.
(This is OK because we know the next inner can't join to this same outer.

Yeah, this is the same optimisation as applies to the other join types
too which happens to just be the same point as semi joins must give up
too.

But we don't know if the next outer can join to this inner.) We advance
inner only when current inner < current outer, so we're done with that
inner and need never rewind. So this is a more fundamental algorithm
change but it gets the same performance benefit.

Yes, I think if inner is unique and outer is unique then after joining
in EXEC_MJ_JOINTUPLES, we can have a new state
EXEC_MJ_NEXTINNERANDOUTER, as the next outer won't match this inner,
so we might as well skip to the next on both, but quite possibly such
a join is just not common enough to have to worry about that. It might
not give us much better performance anyway.

So the question is, if we can skip mark/restore overhead when we know that
either input is unique, is it necessary for the planner to account for
both ways explicitly? Given the general symmetry of mergejoins, it might
be okay for the planner to preferentially generate plans with the unique
input on the inside, and not worry about optimizing in this way when it's
on the outside.

I'm inclined to agree.

Now, JOIN_SEMI and JOIN_ANTI cases are *not* symmetric, since we don't
implement reverse-semi or reverse-anti join modes. But I don't know that
there's anything to be won by noticing that the outer side is unique in
those cases. I think probably we can apply the same technique of
advancing outer not inner, and never rewinding, in those join modes
whether or not either input is known unique.

Another asymmetry is that if one input is likely to be empty, it's better
to put that one on the outside, because if it is empty we don't have to
fetch anything from the other input (thus saving its startup cost). But
the planner doesn't account for that anyway because it never believes
non-dummy relations are truly empty; so even if it's getting this right
today, it's purely by chance.

I can't think of any other asymmetries offhand.

In short, I think you are right that it might be enough to account for
inner uniqueness only, and not worry about it for the outer side, even
for mergejoin. This means my previous objection is wrong and we don't
really have to allow for a future extension of that kind while choosing
the notation the planner uses.

So ... would you rather go back to the previous notation (extra
JoinTypes), or do you like the separate boolean better anyway?

I didn't really ever care for adding the new join types. I think if
someone writes SELECT ... FROM ... INNER JOIN ... and sees a "Semi
Join" in the plan, then we'll be bombarded with bug reports. Also if
we ever changed our minds about outer unique, then we'd not be in a
very great position to go and add it again.

Sorry for jerking you back and forth like this, but sometimes the
correct path isn't apparent from the start.

It's fine. I learned quite a bit today thinking about all this again.

The attached has my Merge Join changes, to show what I think can be
done to make use of unique outer. Let me know what you think, but I
get that idea that we're both leaning towards ripping the outer unique
stuff out, so I'll go do that now...

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-01-27.patchapplication/octet-stream; name=unique_joins_2017-01-27.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index f9fb276..239f78b 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1312,6 +1312,26 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+				ExplainPropertyText("Inner Unique", value, es);
+				value =  ((Join *) plan)->outer_unique ? "Yes" : "No";
+				ExplainPropertyText("Outer Unique", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 6e576ad..1ab7580 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -288,10 +288,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need one inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_inner_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -402,6 +402,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -447,8 +455,11 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			hjstate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 105e2dc..7bdcc82 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -802,10 +802,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need one inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_inner_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1034,14 +1034,26 @@ ExecMergeJoin(MergeJoinState *node)
 
 				/*
 				 * Here we must compare the outer tuple with the marked inner
-				 * tuple.  (We can ignore the result of MJEvalInnerValues,
-				 * since the marked inner tuple is certainly matchable.)
+				 * tuple. When the planner supplied proof of the outer tuples
+				 * being unique based on the join condition, then there's no
+				 * need to make this comparison as this outer tuple must
+				 * certainly be distinct from the previous one, therefore
+				 * cannot match the marked inner tuple.
 				 */
-				innerTupleSlot = node->mj_MarkedTupleSlot;
-				(void) MJEvalInnerValues(node, innerTupleSlot);
+				if (node->js.match_first_outer_tuple_only)
+					compareResult = 1;
+				else
+				{
+					/*
+					 * We can ignore the result of MJEvalInnerValues,
+					 * since the marked inner tuple is certainly matchable.
+					 */
+					innerTupleSlot = node->mj_MarkedTupleSlot;
+					(void) MJEvalInnerValues(node, innerTupleSlot);
 
-				compareResult = MJCompare(node);
-				MJ_DEBUG_COMPARE(compareResult);
+					compareResult = MJCompare(node);
+					MJ_DEBUG_COMPARE(compareResult);
+				}
 
 				if (compareResult == 0)
 				{
@@ -1438,6 +1450,15 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner or outer side of
+	 * the join will at most contain a single tuple for the opposing side of
+	 * the join, then we can use these proofs to provide useful optimzations
+	 * during the Merge Join.
+	 */
+	mergestate->js.match_first_inner_tuple_only = node->join.inner_unique;
+	mergestate->js.match_first_outer_tuple_only = node->join.outer_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1504,8 +1525,11 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			mergestate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index cac7ba1..00ed4c7 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -219,10 +219,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need 1 inner tuple to
+			 * match.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.match_first_inner_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -273,6 +273,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -316,8 +324,11 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			nlstate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 30d733e..226ddb1 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2104,6 +2104,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(inner_unique);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 55c73b7..b3417f8 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -853,6 +853,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(inner_unique);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 1560ac3..8dd0633 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2327,6 +2327,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(inner_unique);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 7c30ec6..49a1fb4 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -103,6 +104,17 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.
+	 */
+	extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+											jointype, sjinfo, restrictlist);
+
+	/* as above but with relations swapped */
+	extra.outer_unique = innerrel_is_unique(root, innerrel, outerrel,
+											jointype, sjinfo, restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -330,11 +342,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -392,11 +402,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -463,10 +471,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -528,11 +535,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -590,11 +595,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 438baf1..87e43e6 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -34,6 +34,8 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+						   SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
@@ -41,6 +43,36 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
+
+
+/*
+ * mark_unique_joins
+ *		Check for proofs on each left joined relation which prove that the
+ *		join can't cause dupliction of RHS tuples.
+ */
+void
+mark_unique_joins(PlannerInfo *root)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs. If we can prove
+		 * these to have a unique inner side, based on the join condition then
+		 * this can save the executor from having to attempt fruitless
+		 * searches for subsequent matching inner tuples.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT && !sjinfo->inner_unique)
+			sjinfo->inner_unique = specialjoin_is_unique_join(root, sjinfo);
+	}
+}
 
 
 /*
@@ -95,6 +127,15 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * mark_unique_joins may have been previously unable to mark a join as
+		 * unique due to there being multiple relations on the RHS of the
+		 * join. We may have just removed a relation so that there's now just
+		 * a singleton relation, so let's try again to mark the joins as
+		 * unique.
+		 */
+		mark_unique_joins(root);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -156,31 +197,22 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	int			innerrelid;
 	RelOptInfo *innerrel;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must have a unique inner side and must be a non-delaying join to a
+	 * single baserel, else we aren't going to be able to do anything with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->inner_unique ||
 		sjinfo->delay_upper_joins)
 		return false;
 
 	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
-		return false;
+		return false;			/* should not happen */
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (!rel_supports_distinctness(root, innerrel))
-		return false;
-
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
 
@@ -190,7 +222,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -231,6 +264,41 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proved that this special join can only ever match at
+ *		most one inner row for any single outer row. False is returned if
+ *		there's insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* if there's more than one relation involved, then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -238,9 +306,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 * it's what we want.  The mergejoinability test also eliminates clauses
 	 * containing volatile functions, which we couldn't depend on.
 	 */
-	foreach(l, innerrel->joininfo)
+	foreach(lc, innerrel->joininfo)
 	{
-		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(l);
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
 
 		/*
 		 * If it's not a join clause for this outer join, we can't use it.
@@ -252,10 +320,11 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we
+			 * can't say we're unique.  (XXX this is confusing conditions for
+			 * join removability with conditions for uniqueness, and could
+			 * probably stand to be improved.  But for the moment, keep on
+			 * applying the stricter condition.)
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -847,3 +916,167 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' is unique based on the join
+ *	  condition in 'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist)
+{
+	int			innerrelid;
+	bool		unique = false;
+
+	/* left joins were already checked by mark_unique_joins */
+	if (jointype == JOIN_LEFT)
+		return sjinfo->inner_unique;
+
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/*
+	 * Any INNER JOINs which can be proven to return at most one inner tuple
+	 * for each outer tuple can be said to be unique.
+	 */
+	if (jointype == JOIN_INNER)
+	{
+		MemoryContext old_context;
+		ListCell   *lc;
+
+		/* can't prove uniqueness for joins with an empty restrictlist */
+		if (restrictlist == NIL)
+			return false;
+
+		/*
+		 * First let's query the unique and non-unique caches to see if we've
+		 * managed to prove that innerrel is unique for some subset of this
+		 * outerrel. We don't need an exact match, as if we have any extra
+		 * outerrels than were previously cached, then they can't make the
+		 * innerrel any less unique.
+		 */
+		foreach(lc, root->unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(unique_rels, outerrel->relids))
+				return true;		/* Success! */
+		}
+
+		/*
+		 * We may have previously determined that this outerrel, or some
+		 * superset thereof, cannot prove this innerrel to be unique.
+		 */
+		foreach(lc, root->non_unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(outerrel->relids, unique_rels))
+				return false;
+		}
+
+		/* No cached information, so try to make the proof. */
+		if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+		{
+			unique = true;		/* Success! */
+
+			/*
+			 * Cache the positive result for future probes, being sure to keep
+			 * it in the planner_cxt even if we are working in GEQO.
+			 *
+			 * Note: one might consider trying to isolate the minimal subset
+			 * of the outerrels that proved the innerrel unique.  But it's not
+			 * worth the trouble, because the planner builds up joinrels
+			 * incrementally and so we'll see the minimally sufficient
+			 * outerrels before any supersets of them anyway.
+			 */
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->unique_rels[innerrelid] =
+				lappend(root->unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+		else
+		{
+			/*
+			 * None of the join conditions for outerrel proved innerrel
+			 * unique, so we can safely reject this outerrel or any subset of
+			 * it in future checks.
+			 *
+			 * However, in normal planning mode, caching this knowledge is
+			 * totally pointless; it won't be queried again, because we build
+			 * up joinrels from smaller to larger.  It is useful in GEQO mode,
+			 * where the knowledge can be carried across successive planning
+			 * attempts; and it's likely to be useful when using join-search
+			 * plugins, too.  Hence cache only when join_search_private is
+			 * non-NULL.  (Yeah, that's a hack, but it seems reasonable.)
+			 */
+			if (root->join_search_private)
+			{
+				old_context = MemoryContextSwitchTo(root->planner_cxt);
+				root->non_unique_rels[innerrelid] =
+					lappend(root->non_unique_rels[innerrelid],
+							bms_copy(outerrel->relids));
+				MemoryContextSwitchTo(old_context);
+			}
+		}
+	}
+	return unique;
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index fae1f67..8904697 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -206,12 +206,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -226,7 +226,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3532,7 +3532,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -3835,7 +3835,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -3975,7 +3975,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5112,7 +5112,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5121,9 +5121,11 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
+	node->join.outer_unique = jpath->outer_unique;
 
 	return node;
 }
@@ -5135,7 +5137,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5145,8 +5147,10 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
+	node->join.outer_unique = jpath->outer_unique;
 
 	return node;
 }
@@ -5187,7 +5191,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5201,8 +5205,10 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
+	node->join.outer_unique = jpath->outer_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index c170e96..5993d0b 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1204,6 +1204,8 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	/* this may be changed later */
+	sjinfo->inner_unique = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index e880759..46dbe76 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
@@ -185,6 +188,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* check for unique properties on outer joins */
+	mark_unique_joins(root);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index f440875..6ebbcff 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1935,11 +1935,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -1950,16 +1948,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -1995,7 +1992,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2008,8 +2005,10 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
+	pathnode->outer_unique = extra->outer_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra->sjinfo, &extra->semifactors);
 
 	return pathnode;
 }
@@ -2022,10 +2021,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2038,10 +2036,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2049,6 +2046,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2058,7 +2056,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2071,12 +2069,14 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
+	pathnode->jpath.outer_unique = extra->outer_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra->sjinfo);
 
 	return pathnode;
 }
@@ -2088,11 +2088,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2102,15 +2100,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2120,7 +2117,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2145,10 +2142,12 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
+	pathnode->jpath.outer_unique = extra->outer_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra->sjinfo, &extra->semifactors);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index adc1db9..d1bda16 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -81,6 +81,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f9bcdd6..8ce590d 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1669,6 +1669,17 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		match_first_inner_tuple_only;	/* True if we can safely move
+												 * to the next outer tuple
+												 * after matching first inner
+												 * tuple
+												 */
+	bool		match_first_outer_tuple_only;	/* True if we can safely move
+												 * to the next inner tuple
+												 * after matching first outer
+												 * tuple
+												 */
+
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index f72f7a8..ce2285e 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -618,6 +618,8 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
+	bool		outer_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 643be54..1c7a609 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -222,6 +222,19 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner or outer sides based on the join
+	 * condition. This is a rather expensive test to perform, as it requires
+	 * checking each relation's unique indexes to see if the relation can, at
+	 * most, return one tuple for each opposing tuple in the join. We use this
+	 * cache during the join search to record lists of the sets of relations
+	 * which both prove, and disprove the uniqueness properties for the relid
+	 * indexed by these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1217,6 +1230,11 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* inner side of join matches no more than one
+								 * outer side tuple */
+	bool		outer_unique;	/* outer side of join matches no more than one
+								 * inner side tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -1810,6 +1828,8 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		inner_unique;	/* each outerside tuple can, at most match to
+								 * a single inner side tuple. */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -2042,6 +2062,10 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique marks if the inner side of join matches no more than one outer
+ * side tuple
+ * outer_unique marks if the outer side of join matches no more than one inner
+ * side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2050,6 +2074,8 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
+	bool		outer_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 7b41317..5ce410c 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -102,11 +102,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -114,10 +112,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -128,11 +125,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					  JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -238,6 +233,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..128753a 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -102,10 +102,15 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
-
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist);
 /*
  * prototypes for plan/setrefs.c
  */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..7604d20 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,8 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
+         Outer Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +392,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(16 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index d9bbae0..233c796 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,8 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3371,14 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
+               Outer Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(18 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3416,13 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3430,13 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
+                           Outer Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3452,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(38 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3485,13 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3499,18 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
+                           Outer Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
+                                 Outer Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3527,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(44 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3561,13 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3575,18 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
+                           Outer Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
+                                 Outer Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3606,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(47 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3638,20 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
+               Outer Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
+                     Outer Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3661,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3691,13 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
+         Outer Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3709,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(20 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3734,17 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
+         Outer Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
+               Outer Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3760,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(28 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3786,16 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
+   Outer Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
+         Outer Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
+               Outer Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3805,8 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
+                     Outer Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3821,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(37 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3851,21 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
+   Outer Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
+               Outer Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
+                     Outer Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3882,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(33 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3970,8 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
+   Outer Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3984,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(16 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -4801,12 +4867,14 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(9 rows)
 
 select * from
   int8_tbl a left join
@@ -4833,12 +4901,14 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(9 rows)
 
 select * from
   int8_tbl a left join
@@ -4866,6 +4936,8 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
+   Outer Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4873,7 +4945,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(11 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4893,12 +4965,14 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(9 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4920,10 +4994,14 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4932,7 +5010,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(18 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4978,16 +5056,20 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
+         Outer Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(15 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5054,13 +5136,21 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
+   Outer Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
+               Outer Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5076,7 +5166,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(32 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5094,17 +5184,27 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
+         Outer Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
+               Outer Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
+                     Outer Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
+                           Outer Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5126,7 +5226,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(44 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5139,16 +5239,20 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
+         Outer Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(15 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5161,10 +5265,14 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
+   Outer Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5173,7 +5281,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(18 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5200,10 +5308,14 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
+         Outer Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5225,7 +5337,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(31 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5323,3 +5435,255 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Outer Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(11 rows)
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(11 rows)
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Outer Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Unique
+         Output: j3.id
+         ->  Sort
+               Output: j3.id
+               Sort Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(14 rows)
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Group
+         Output: j3.id
+         Group Key: j3.id
+         ->  Sort
+               Output: j3.id
+               Sort Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(15 rows)
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Outer Unique: No
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(11 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of join conversions
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Outer Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(9 rows)
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Outer Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 79513e4..7e948ed 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,14 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
+   Outer Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(9 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 56481de..434de62 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2010,12 +2010,14 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(9 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2036,11 +2038,13 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
+   Outer Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 47afdc3..cf5905c 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -814,6 +814,8 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -822,7 +824,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(12 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -842,6 +844,8 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
+   Outer Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -859,7 +863,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(21 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 3b7f689..d121382 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2195,6 +2195,8 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2202,6 +2204,8 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2209,6 +2213,8 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2216,12 +2222,14 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
+         Outer Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(46 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 97bccec..69e0109 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1730,3 +1730,95 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is changed to a semi join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join not changed when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- don't change, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is converted to left semi join
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is converted too
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be converted
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is converted to a semi join
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure distinct clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become a semi join
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- ensure a full join is not altered
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of join conversions
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no join conversion when not all columns which are part of
+-- the unique index are part of the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure inner is converted to semi join when there's multiple columns in the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#93David Rowley
david.rowley@2ndquadrant.com
In reply to: Antonin Houska (#91)
Re: Performance improvement for joins where outer side is unique

On 26 January 2017 at 04:56, Antonin Houska <ah@cybertec.at> wrote:

I suspect that "inner" and "outer" relation / tuple are sometimes confused in
comments:

* analyzejoins.c:70

"searches for subsequent matching outer tuples."

* analyzejoins.c:972

/*
* innerrel_is_unique
* Check for proofs which prove that 'innerrel' can, at most, match a
* single tuple in 'outerrel' based on the join condition in
* 'restrictlist'.
*/

* relation.h:1831

bool inner_unique; /* inner side of join matches no more than one
* outer side tuple */

Thanks for looking over the patch. I believe I've fixed all those now.
I'm not too surprised I got some wrong, after all, I did mess up the
subject of this email too! Which did cause quite a bit of confusion.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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

#94David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#92)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 27 January 2017 at 00:37, David Rowley <david.rowley@2ndquadrant.com> wrote:

The attached has my Merge Join changes, to show what I think can be
done to make use of unique outer. Let me know what you think, but I
get that idea that we're both leaning towards ripping the outer unique
stuff out, so I'll go do that now...

I've attached a version without outer unique.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-01-27_no_outer_unique.patchapplication/octet-stream; name=unique_joins_2017-01-27_no_outer_unique.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index f9fb276..64c4217 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1312,6 +1312,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+				ExplainPropertyText("Inner Unique", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 6e576ad..1ab7580 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -288,10 +288,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need one inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_inner_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -402,6 +402,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -447,8 +455,11 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			hjstate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 105e2dc..2bdd766 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -802,10 +802,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need one inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_inner_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1438,6 +1438,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1504,8 +1512,11 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			mergestate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index cac7ba1..00ed4c7 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -219,10 +219,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need 1 inner tuple to
+			 * match.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.match_first_inner_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -273,6 +273,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -316,8 +324,11 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			nlstate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 30d733e..226ddb1 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2104,6 +2104,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(inner_unique);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 55c73b7..b3417f8 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -853,6 +853,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(inner_unique);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 1560ac3..8dd0633 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2327,6 +2327,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(inner_unique);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 7c30ec6..0762aa4 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -103,6 +104,13 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.
+	 */
+	extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+											jointype, sjinfo, restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -330,11 +338,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -392,11 +398,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -463,10 +467,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -528,11 +531,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -590,11 +591,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 438baf1..51ebe06 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -34,6 +34,8 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+						   SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
@@ -41,6 +43,36 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
+
+
+/*
+ * mark_unique_joins
+ *		Check for proofs on each left joined relation which prove that the
+ *		join can't cause dupliction of RHS tuples.
+ */
+void
+mark_unique_joins(PlannerInfo *root)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs. If we can prove
+		 * these to have a unique inner side, based on the join condition then
+		 * this can save the executor from having to attempt fruitless
+		 * searches for subsequent matching inner tuples.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT && !sjinfo->inner_unique)
+			sjinfo->inner_unique = specialjoin_is_unique_join(root, sjinfo);
+	}
+}
 
 
 /*
@@ -95,6 +127,15 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * mark_unique_joins may have been previously unable to mark a join as
+		 * unique due to there being multiple relations on the RHS of the
+		 * join. We may have just removed a relation so that there's now just
+		 * a singleton relation, so let's try again to mark the joins as
+		 * unique.
+		 */
+		mark_unique_joins(root);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -156,31 +197,22 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	int			innerrelid;
 	RelOptInfo *innerrel;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must have a unique inner side and must be a non-delaying join to a
+	 * single baserel, else we aren't going to be able to do anything with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->inner_unique ||
 		sjinfo->delay_upper_joins)
 		return false;
 
 	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
-		return false;
+		return false;			/* should not happen */
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (!rel_supports_distinctness(root, innerrel))
-		return false;
-
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
 
@@ -190,7 +222,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -231,6 +264,41 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proven that this special join can only ever match at
+ *		most one inner tuple for any single outer tuple. False is returned if
+ *		there's insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* if there's more than one relation involved, then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -238,9 +306,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 * it's what we want.  The mergejoinability test also eliminates clauses
 	 * containing volatile functions, which we couldn't depend on.
 	 */
-	foreach(l, innerrel->joininfo)
+	foreach(lc, innerrel->joininfo)
 	{
-		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(l);
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
 
 		/*
 		 * If it's not a join clause for this outer join, we can't use it.
@@ -252,10 +320,11 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we
+			 * can't say we're unique.  (XXX this is confusing conditions for
+			 * join removability with conditions for uniqueness, and could
+			 * probably stand to be improved.  But for the moment, keep on
+			 * applying the stricter condition.)
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -847,3 +916,167 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' is unique based on the join
+ *	  condition in 'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist)
+{
+	int			innerrelid;
+	bool		unique = false;
+
+	/* left joins were already checked by mark_unique_joins */
+	if (jointype == JOIN_LEFT)
+		return sjinfo->inner_unique;
+
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/*
+	 * Any INNER JOINs which can be proven to return at most one inner tuple
+	 * for each outer tuple can be said to be unique.
+	 */
+	if (jointype == JOIN_INNER)
+	{
+		MemoryContext old_context;
+		ListCell   *lc;
+
+		/* can't prove uniqueness for joins with an empty restrictlist */
+		if (restrictlist == NIL)
+			return false;
+
+		/*
+		 * First let's query the unique and non-unique caches to see if we've
+		 * managed to prove that innerrel is unique for some subset of this
+		 * outerrel. We don't need an exact match, as if we have any extra
+		 * outerrels than were previously cached, then they can't make the
+		 * innerrel any less unique.
+		 */
+		foreach(lc, root->unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(unique_rels, outerrel->relids))
+				return true;		/* Success! */
+		}
+
+		/*
+		 * We may have previously determined that this outerrel, or some
+		 * superset thereof, cannot prove this innerrel to be unique.
+		 */
+		foreach(lc, root->non_unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(outerrel->relids, unique_rels))
+				return false;
+		}
+
+		/* No cached information, so try to make the proof. */
+		if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+		{
+			unique = true;		/* Success! */
+
+			/*
+			 * Cache the positive result for future probes, being sure to keep
+			 * it in the planner_cxt even if we are working in GEQO.
+			 *
+			 * Note: one might consider trying to isolate the minimal subset
+			 * of the outerrels that proved the innerrel unique.  But it's not
+			 * worth the trouble, because the planner builds up joinrels
+			 * incrementally and so we'll see the minimally sufficient
+			 * outerrels before any supersets of them anyway.
+			 */
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->unique_rels[innerrelid] =
+				lappend(root->unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+		else
+		{
+			/*
+			 * None of the join conditions for outerrel proved innerrel
+			 * unique, so we can safely reject this outerrel or any subset of
+			 * it in future checks.
+			 *
+			 * However, in normal planning mode, caching this knowledge is
+			 * totally pointless; it won't be queried again, because we build
+			 * up joinrels from smaller to larger.  It is useful in GEQO mode,
+			 * where the knowledge can be carried across successive planning
+			 * attempts; and it's likely to be useful when using join-search
+			 * plugins, too.  Hence cache only when join_search_private is
+			 * non-NULL.  (Yeah, that's a hack, but it seems reasonable.)
+			 */
+			if (root->join_search_private)
+			{
+				old_context = MemoryContextSwitchTo(root->planner_cxt);
+				root->non_unique_rels[innerrelid] =
+					lappend(root->non_unique_rels[innerrelid],
+							bms_copy(outerrel->relids));
+				MemoryContextSwitchTo(old_context);
+			}
+		}
+	}
+	return unique;
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index fae1f67..c60c2c9 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -206,12 +206,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -226,7 +226,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3532,7 +3532,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -3835,7 +3835,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -3975,7 +3975,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5112,7 +5112,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5121,9 +5121,10 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5135,7 +5136,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5145,8 +5146,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5187,7 +5189,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5201,8 +5203,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index c170e96..5993d0b 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1204,6 +1204,8 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	/* this may be changed later */
+	sjinfo->inner_unique = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index e880759..46dbe76 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
@@ -185,6 +188,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* check for unique properties on outer joins */
+	mark_unique_joins(root);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index f440875..11e37ac 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1935,11 +1935,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -1950,16 +1948,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -1995,7 +1992,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2008,8 +2005,9 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra->sjinfo, &extra->semifactors);
 
 	return pathnode;
 }
@@ -2022,10 +2020,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2038,10 +2035,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2049,6 +2045,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2058,7 +2055,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2071,12 +2068,13 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra->sjinfo);
 
 	return pathnode;
 }
@@ -2088,11 +2086,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2102,15 +2098,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2120,7 +2115,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2145,10 +2140,11 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra->sjinfo, &extra->semifactors);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index adc1db9..d1bda16 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -81,6 +81,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f9bcdd6..a3f936d 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1669,6 +1669,10 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		match_first_inner_tuple_only;	/* True if we can safely move
+												 * to the next outer tuple
+												 * after matching first inner
+												 * tuple.*/
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index f72f7a8..6c2bc35 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -603,6 +603,8 @@ typedef struct CustomScan
  * jointype:	rule for joining tuples from left and right subtrees
  * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
  *				(plan.qual contains conditions that came from WHERE)
+ * inner_unique	outer tuples can only match to no more than one inner
+ *				tuple
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -618,6 +620,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 643be54..e195a7c 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -222,6 +222,18 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner side based on the join condition.
+	 * This is a rather expensive test to perform, and requires looking fo
+	 * proofs such as a relation's unique indexes.  We use this cache during
+	 * the join search to record lists of the sets of relations which both
+	 * prove, and disprove the uniqueness properties for the relid indexed by
+	 * these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1217,6 +1229,9 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* outer side of join matches no more than one
+								 * inner side tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -1810,6 +1825,8 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		inner_unique;	/* outer side of join matches no more than one
+								 * inner side tuple */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -2042,6 +2059,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique true if proofs exist which prove that outer side tuples match
+ *		no more than one inner side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2050,6 +2069,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 7b41317..5ce410c 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -102,11 +102,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -114,10 +112,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -128,11 +125,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					  JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -238,6 +233,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..128753a 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -102,10 +102,15 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
-
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist);
 /*
  * prototypes for plan/setrefs.c
  */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..95f9303 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,7 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +391,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index d9bbae0..71cff74 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3370,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3414,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3426,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3446,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3479,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3491,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3516,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3550,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3562,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3590,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3622,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3642,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3672,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3688,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3713,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3736,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3762,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3778,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3793,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3823,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3850,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3938,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3951,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -4801,12 +4834,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4833,12 +4867,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4866,6 +4901,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4873,7 +4909,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4893,12 +4929,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4920,10 +4957,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4932,7 +4971,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4978,16 +5017,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5054,13 +5095,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5076,7 +5121,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5094,17 +5139,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5126,7 +5176,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5139,16 +5189,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5161,10 +5213,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5173,7 +5227,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5200,10 +5254,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5225,7 +5281,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5323,3 +5379,242 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Unique
+         Output: j3.id
+         ->  Sort
+               Output: j3.id
+               Sort Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(13 rows)
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+               QUERY PLAN                
+-----------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Group
+         Output: j3.id
+         Group Key: j3.id
+         ->  Sort
+               Output: j3.id
+               Sort Key: j3.id
+               ->  Seq Scan on public.j3
+                     Output: j3.id
+   ->  Seq Scan on public.j1
+         Output: j1.id
+(14 rows)
+
+-- ensure a full join is not unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 79513e4..d7d2376 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,13 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(8 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 56481de..ff9cc77 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2010,12 +2010,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2036,11 +2037,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 47afdc3..071daf5 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -814,6 +814,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -822,7 +823,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -842,6 +843,7 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -859,7 +861,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 3b7f689..3e4c3fb 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2195,6 +2195,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2202,6 +2203,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2209,6 +2211,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2216,12 +2219,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 97bccec..a727829 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1730,3 +1730,94 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+-- ensure a full join is not unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#95Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#94)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

I've attached a version without outer unique.

I looked through this a bit, and the first thing I noticed was it doesn't
touch costsize.c at all. That seems pretty wrong; it's little help to
have a performance improvement if the planner won't pick the right plan
type. There were costsize.c changes in there back in April; what happened
to them?

regards, tom lane

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

#96David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#95)
Re: Performance improvement for joins where outer side is unique

On 27 January 2017 at 08:34, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

I've attached a version without outer unique.

I looked through this a bit, and the first thing I noticed was it doesn't
touch costsize.c at all. That seems pretty wrong; it's little help to
have a performance improvement if the planner won't pick the right plan
type. There were costsize.c changes in there back in April; what happened
to them?

hmm. I must've taken them out when I changed everything around to use
the join types. Nothing special was needed when changing JOIN_INNER to
JOIN_SEMI.

I must've forgotten to put them back again... I'll do that now.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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

#97Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#96)
Re: Performance improvement for joins where outer side is unique

To re-familiarize myself with this patch, I've been re-reading the thread,
which has gotten quite long. It seemed like it would be a good idea to
stop and try to summarize what the patch ought to accomplish, because
there's been some drift over the more than 2 years the patch has been
in the works.

So: ISTM there are two core ideas at this point.

1. We should recognize when the inner side of a join is unique (that is,
there is provably no more than one inner tuple joining to any given outer
tuple), and make use of that knowledge to short-circuit execution in the
same way we already do for JOIN_SEMI and JOIN_ANTI cases. That is, once
we find a match we can immediately move on to the next outer tuple rather
than continuing to scan for inner-side matches.

2. In these same cases (unique/semi/anti joins), it is possible to avoid
mark/restore overhead in a mergejoin, because we can tweak the executor
logic to not require backing up the inner side. This goes further than
just tweaking the executor logic, though, because if we know we don't
need mark/restore then that actually makes some plan shapes legal that
weren't before: we don't need to interpose a Material node to protect
join inputs that can't mark/restore.

Maybe I missed something, but it doesn't look like the current patch
(unique_joins_2017-01-27_no_outer_unique.patch) has anything concerning
point #2 at all. It might make sense to address that idea as a follow-on
patch, but I think it can be a quite significant win and we shouldn't
just lose track of it.

Anyway, having laid out that scope of work, I have some concerns:

* The patch applies point #1 to only INNER and LEFT join modes, but
I don't really see why the idea wouldn't work for RIGHT and FULL modes,
ie the optimization seems potentially interesting for all executable join
types. Once you've got a match, you can immediately go to the next outer
tuple instead of continuing to scan inner. (Am I missing something?)

* Particularly in view of the preceding point, I'm not that happy with
the way that management/caching of the "is it unique" knowledge is
done completely differently for INNER and LEFT joins. I wonder if
there's actually a good argument for that or is it mostly a development
sequence artifact. IOW, would it hurt to drop the SpecialJoinInfo
tie-in and just rely on the generic cache?

* Because of where we apply the short-circuit logic in the executor,
it's only safe to consider the inner rel as unique if it is provably
unique using only the join clauses that drive the primary join mechanism
(ie, the "joinquals" not the "otherquals"). We already do ignore quals
that are pushed-down to an outer join, so that's good, but there's an
oversight: we will use a qual that is mergejoinable even if it's not
hashjoinable. That means we could get the wrong answers in a hash join.
I think probably the appropriate fix for the moment is just to consider
only clauses that are both mergeable and hashable while trying to prove
uniqueness. We do have some equality operators that support only one
or the other, but they're corner cases, and I'm dubious that it's worth
having to make separate proofs for merge and hash joins in order to
cater to those cases.

regards, tom lane

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

#98Antonin Houska
ah@cybertec.at
In reply to: David Rowley (#59)
Re: Performance improvement for joins where outer side is unique

I thought about the patch from the perspective of "grouped relations"
(especially [1]/messages/by-id/CAKJS1f_h1CLff92B=+bdrMK2Nf3EfGWaJu2WbzQUYcSBUi02ag@mail.gmail.com). When looking for the appropriate context within the thread,
I picked this message.

David Rowley <david.rowley@2ndquadrant.com> wrote:

On 12 March 2016 at 11:43, Tom Lane <tgl@sss.pgh.pa.us> wrote:

It seems like the major intellectual complexity here is to figure out
how to detect inner-side-unique at reasonable cost. I see that for
LEFT joins you're caching that in the SpecialJoinInfos, which is probably
fine. But for INNER joins it looks like you're just doing it over again
for every candidate join, and that seems mighty expensive.

... I'll look into that.

The other thing I thought of was to add a dedicated list for unique
indexes in RelOptInfo, this would also allow
rel_supports_distinctness() to do something a bit smarter than just
return false if there's no indexes. That might not buy us much though,
but at least relations tend to have very little unique indexes, even
when they have lots of indexes.

I'm thinking of a concept of "unique keys", similar to path keys that the
planner already uses. Besides the current evaluation of uniqueness of the
inner side of a join, the planner would (kind of) union the unique keys of the
joined rels, ie compute a list of expressions which generates an unique row
throughout the new join result. (Requirement is that each key must be usable
in join expression, as opposed to filter.)

To figure out whether at most one inner row exists per outer row, each unique
key of the inner relation which references the outer relation needs to match
an unique key of the outer relation (but it's probably wrong if multiple
unique keys of the inner rel reference the same key of the outer rel).

Like path key, the unique key would also point to an equivalence class. Thus
mere equality of the EC pointers could perhaps be used to evaluate the match
of the inner and outer keys.

Given that rel_is_distinct_for() currently does not accept joins, this change
would make the patch more generic. (BTW, with this approach, unique_rels and
non_unique_rels caches would have to be stored per-relation (RelOptInfo), as
opposed to PlannerInfo.)

The reason I'd like the unique keys is that - from the "grouped relation"
point of view - the relation uniqueness also needs to be checked against the
GROUP BY clause. Thus the "unique keys" concept seem to me like an useful
abstraction.

Does this proposal seem to have a serious flaw?

[1]: /messages/by-id/CAKJS1f_h1CLff92B=+bdrMK2Nf3EfGWaJu2WbzQUYcSBUi02ag@mail.gmail.com
/messages/by-id/CAKJS1f_h1CLff92B=+bdrMK2Nf3EfGWaJu2WbzQUYcSBUi02ag@mail.gmail.com

--
Antonin Houska
Cybertec Schönig & Schönig GmbH
Gröhrmühlgasse 26
A-2700 Wiener Neustadt
Web: http://www.postgresql-support.de, http://www.cybertec.at

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

#99David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#97)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 27 January 2017 at 12:39, Tom Lane <tgl@sss.pgh.pa.us> wrote:

2. In these same cases (unique/semi/anti joins), it is possible to avoid
mark/restore overhead in a mergejoin, because we can tweak the executor
logic to not require backing up the inner side. This goes further than
just tweaking the executor logic, though, because if we know we don't
need mark/restore then that actually makes some plan shapes legal that
weren't before: we don't need to interpose a Material node to protect
join inputs that can't mark/restore.

I've made modifications in the attached to add this optimization, and
it's quite a significant improvement.

Setup:

create table t1 (id text primary key);
insert into t1 select x from generate_series(1,1000000) x(x);
create table t2 (id text primary key);
insert into t2 select x from generate_series(1,1000000) x(x);
vacuum freeze;

Query:
select count(*) from t1 inner join t2 on t1.id=t2.id;

Unpatched

Time: 369.618 ms
Time: 358.208 ms
Time: 357.094 ms
Time: 355.642 ms
Time: 358.193 ms
Time: 371.272 ms
Time: 354.386 ms
Time: 364.277 ms
Time: 346.091 ms
Time: 358.757 ms

Patched

Time: 273.154 ms
Time: 258.520 ms
Time: 269.456 ms
Time: 252.861 ms
Time: 271.015 ms
Time: 252.567 ms
Time: 271.132 ms
Time: 267.505 ms
Time: 265.295 ms
Time: 257.068 ms

About 26.5% improvement for this case.

* The patch applies point #1 to only INNER and LEFT join modes, but
I don't really see why the idea wouldn't work for RIGHT and FULL modes,
ie the optimization seems potentially interesting for all executable join
types. Once you've got a match, you can immediately go to the next outer
tuple instead of continuing to scan inner. (Am I missing something?)

No I believe I started development with LEFT JOINs, moved to INNER,
then didn't progress to think of RIGHT or FULL. I've lifted this
restriction in the patch. It seems no other work is required to have
that just work.

* Particularly in view of the preceding point, I'm not that happy with
the way that management/caching of the "is it unique" knowledge is
done completely differently for INNER and LEFT joins. I wonder if
there's actually a good argument for that or is it mostly a development
sequence artifact. IOW, would it hurt to drop the SpecialJoinInfo
tie-in and just rely on the generic cache?

I agree that special handling of one join type is not so pretty.
However, LEFT JOINs still remain a bit special as they're the only
ones we currently perform join removal on, and the patch modifies that
code to make use of the new flag for those. This can improve planner
performance of join removal when a join is removed successfully, as
the previous code had to recheck uniqueness of each remaining LEFT
JOIN again, whereas the new code only checks uniqueness of ones not
previously marked as unique. This too likely could be done with the
cache, although I'm a bit concerned with populating the cache, then
performing a bunch of LEFT JOIN removals and leaving relids in the
cache which no longer exist. Perhaps it's OK. I've just not found
proofs in my head yet that it is.

* Because of where we apply the short-circuit logic in the executor,
it's only safe to consider the inner rel as unique if it is provably
unique using only the join clauses that drive the primary join mechanism
(ie, the "joinquals" not the "otherquals"). We already do ignore quals
that are pushed-down to an outer join, so that's good, but there's an
oversight: we will use a qual that is mergejoinable even if it's not
hashjoinable. That means we could get the wrong answers in a hash join.
I think probably the appropriate fix for the moment is just to consider
only clauses that are both mergeable and hashable while trying to prove
uniqueness. We do have some equality operators that support only one
or the other, but they're corner cases, and I'm dubious that it's worth
having to make separate proofs for merge and hash joins in order to
cater to those cases.

hmm. I'm having trouble understanding why this is a problem for Unique
joins, but not for join removal?

However you mentioning this cause me to notice that this is only true
in the patch for left joins, and the other join types are not
consistent with that.

create table a1 (a int, b int, primary key(a,b));
create table a2 (a int, b int, primary key(a,b));

explain (verbose, costs off) select * from a1 left join a2 on a1.a =
a2.a and a2.b=1 ;
QUERY PLAN
--------------------------------------------------
Merge Left Join
Output: a1.a, a1.b, a2.a, a2.b
Inner Unique: Yes
Merge Cond: (a1.a = a2.a)
-> Index Only Scan using a1_pkey on public.a1
Output: a1.a, a1.b
-> Sort
Output: a2.a, a2.b
Sort Key: a2.a
-> Bitmap Heap Scan on public.a2
Output: a2.a, a2.b
Recheck Cond: (a2.b = 1)
-> Bitmap Index Scan on a2_pkey
Index Cond: (a2.b = 1)

explain (verbose, costs off) select * from a1 inner join a2 on a1.a =
a2.a and a2.b=1 ;
QUERY PLAN
--------------------------------------------------
Nested Loop
Output: a1.a, a1.b, a2.a, a2.b
Inner Unique: No
-> Bitmap Heap Scan on public.a2
Output: a2.a, a2.b
Recheck Cond: (a2.b = 1)
-> Bitmap Index Scan on a2_pkey
Index Cond: (a2.b = 1)
-> Index Only Scan using a1_pkey on public.a1
Output: a1.a, a1.b
Index Cond: (a1.a = a2.a)

Notice the inner join is not detected as unique, but the left join is.

This is still not right in the attached. I'm not quite sure what to do
about it yet.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-01-28.patchapplication/octet-stream; name=unique_joins_2017-01-28.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index f9fb276..64c4217 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1312,6 +1312,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+				ExplainPropertyText("Inner Unique", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 6e576ad..1ab7580 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -288,10 +288,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need one inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_inner_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -402,6 +402,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -447,8 +455,11 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			hjstate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 105e2dc..03670af 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -802,10 +802,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need one inner
+					 * tuple to match.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.match_first_inner_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -1048,7 +1048,11 @@ ExecMergeJoin(MergeJoinState *node)
 					/*
 					 * the merge clause matched so now we restore the inner
 					 * scan position to the first mark, and go join that tuple
-					 * (and any following ones) to the new outer.
+					 * (and any following ones) to the new outer. In the case
+					 * where we've only joined to the first inner tuple, we've
+					 * not advanced the inner side any away from where the
+					 * restore point would normally be, so have no need to
+					 * perform the restore.
 					 *
 					 * NOTE: we do not need to worry about the MatchedInner
 					 * state for the rescanned inner tuples.  We know all of
@@ -1062,17 +1066,20 @@ ExecMergeJoin(MergeJoinState *node)
 					 * forcing the merge clause to never match, so we never
 					 * get here.
 					 */
-					ExecRestrPos(innerPlan);
+					if (!node->js.match_first_inner_tuple_only)
+					{
+						ExecRestrPos(innerPlan);
 
-					/*
-					 * ExecRestrPos probably should give us back a new Slot,
-					 * but since it doesn't, use the marked slot.  (The
-					 * previously returned mj_InnerTupleSlot cannot be assumed
-					 * to hold the required tuple.)
-					 */
-					node->mj_InnerTupleSlot = innerTupleSlot;
-					/* we need not do MJEvalInnerValues again */
+						/*
+						 * ExecRestrPos probably should give us back a new
+						 * Slot, but since it doesn't, use the marked slot.
+						 * (The previously returned mj_InnerTupleSlot cannot
+						 * be assumed to hold the required tuple.)
+						 */
+						node->mj_InnerTupleSlot = innerTupleSlot;
+					}
 
+					/* we need not do MJEvalInnerValues again */
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
@@ -1172,9 +1179,17 @@ ExecMergeJoin(MergeJoinState *node)
 
 				if (compareResult == 0)
 				{
-					ExecMarkPos(innerPlan);
+					/*
+					 * When we're only going join to a single inner tuple then
+					 * we'll not advance the inner side further on the current
+					 * outer tuple, so have no need to perform a Mark.
+					 */
+					if (!node->js.match_first_inner_tuple_only)
+					{
+						ExecMarkPos(innerPlan);
 
-					MarkInnerTuple(node->mj_InnerTupleSlot, node);
+						MarkInnerTuple(node->mj_InnerTupleSlot, node);
+					}
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
@@ -1438,6 +1453,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1504,8 +1527,11 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			mergestate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index cac7ba1..00ed4c7 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -219,10 +219,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need 1 inner tuple to
+			 * match.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.match_first_inner_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -273,6 +273,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.match_first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -316,8 +324,11 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/* for semi joins we match to the first inner tuple only */
+			nlstate->js.match_first_inner_tuple_only = true;
+			/* fall through */
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 30d733e..226ddb1 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2104,6 +2104,7 @@ _copySpecialJoinInfo(const SpecialJoinInfo *from)
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(lhs_strict);
 	COPY_SCALAR_FIELD(delay_upper_joins);
+	COPY_SCALAR_FIELD(inner_unique);
 	COPY_SCALAR_FIELD(semi_can_btree);
 	COPY_SCALAR_FIELD(semi_can_hash);
 	COPY_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 55c73b7..b3417f8 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -853,6 +853,7 @@ _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(lhs_strict);
 	COMPARE_SCALAR_FIELD(delay_upper_joins);
+	COMPARE_SCALAR_FIELD(inner_unique);
 	COMPARE_SCALAR_FIELD(semi_can_btree);
 	COMPARE_SCALAR_FIELD(semi_can_hash);
 	COMPARE_NODE_FIELD(semi_operators);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 1560ac3..8dd0633 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2327,6 +2327,7 @@ _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 	WRITE_ENUM_FIELD(jointype, JoinType);
 	WRITE_BOOL_FIELD(lhs_strict);
 	WRITE_BOOL_FIELD(delay_upper_joins);
+	WRITE_BOOL_FIELD(inner_unique);
 	WRITE_BOOL_FIELD(semi_can_btree);
 	WRITE_BOOL_FIELD(semi_can_hash);
 	WRITE_NODE_FIELD(semi_operators);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 458f139..0f28bbe 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1889,15 +1889,13 @@ cost_group(Path *path, PlannerInfo *root,
  * 'jointype' is the type of join to be performed
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -1928,10 +1926,13 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With unique inner, SEMI or ANTI join, the executor will stop after
+		 * first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -1964,17 +1965,16 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  *
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->outerjoinpath;
 	Path	   *inner_path = path->innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	Cost		startup_cost = workspace->startup_cost;
@@ -2009,10 +2009,13 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With unique inner, SEMI or ANTI join, the executor will stop after
+		 * first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2153,7 +2156,7 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
  * 'inner_path' is the inner input to the join
  * 'outersortkeys' is the list of sort keys for the outer path
  * 'innersortkeys' is the list of sort keys for the inner path
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  *
  * Note: outersortkeys and innersortkeys should be NIL if no explicit
  * sort is needed because the respective source path is already ordered.
@@ -2164,7 +2167,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo)
+					   JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2379,12 +2382,12 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		materialize_inner
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo)
+					 JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
@@ -2468,9 +2471,14 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * computations?
 	 *
 	 * The whole issue is moot if we are working from a unique-ified outer
-	 * input.
+	 * input. We'll also never rescan during SEMI/ANTI/inner_unique as there's
+	 * no need to advance the inner side to find subsequent matches, therefore
+	 * no need to mark/restore.
 	 */
-	if (IsA(outer_path, UniquePath))
+	if (IsA(outer_path, UniquePath) ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		extra->inner_unique)
 		rescannedtuples = 0;
 	else
 	{
@@ -2519,7 +2527,10 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	/*
 	 * Even if materializing doesn't look cheaper, we *must* do it if the
 	 * inner path is to be used directly (without sorting) and it doesn't
-	 * support mark/restore.
+	 * support mark/restore. However, if the inner side of the join is
+	 * guaranteed not to duplicate outer rows, as in the case with SEMI/ANTI
+	 * JOINs and joins which have an inner unique side, then we'll never need
+	 * to perform mark/restore.
 	 *
 	 * Since the inner side must be ordered, and only Sorts and IndexScans can
 	 * create order to begin with, and they both support mark/restore, you
@@ -2532,7 +2543,9 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * materialization is required for correctness in this case, and turning
 	 * it off does not entitle us to deliver an invalid plan.
 	 */
-	else if (innersortkeys == NIL &&
+	else if (innersortkeys == NIL && !extra->inner_unique &&
+			 path->jpath.jointype != JOIN_SEMI &&
+			 path->jpath.jointype != JOIN_ANTI &&
 			 !ExecSupportsMarkRestore(inner_path))
 		path->materialize_inner = true;
 
@@ -2674,16 +2687,14 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
  * 'hashclauses' is the list of joinclauses to be used as hash clauses
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2768,17 +2779,16 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	List	   *hashclauses = path->path_hashclauses;
@@ -2896,13 +2906,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With unique inner, SEMI or ANTI join, the executor will stop after
+		 * first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 7c30ec6..6367a2c 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -103,6 +104,13 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.
+	 */
+	extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+											jointype, sjinfo, restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -318,8 +326,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * methodology worthwhile.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -330,11 +337,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -381,8 +386,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
 
@@ -392,11 +396,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -452,7 +454,7 @@ try_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -463,10 +465,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -516,8 +517,7 @@ try_hashjoin_path(PlannerInfo *root,
 	 * never have any output pathkeys, per comments in create_hashjoin_path.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -528,11 +528,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -579,8 +577,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, NIL))
 		return;
 
@@ -590,11 +587,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 438baf1..c546def 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -34,6 +34,8 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+						   SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
@@ -41,6 +43,36 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
+
+
+/*
+ * mark_unique_joins
+ *		Check for proofs on each left joined relation which prove that the
+ *		join can't cause dupliction of RHS tuples.
+ */
+void
+mark_unique_joins(PlannerInfo *root)
+{
+	ListCell   *lc;
+
+	foreach(lc, root->join_info_list)
+	{
+		SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
+
+		/*
+		 * Currently we're only interested in LEFT JOINs. If we can prove
+		 * these to have a unique inner side, based on the join condition then
+		 * this can save the executor from having to attempt fruitless
+		 * searches for subsequent matching inner tuples.
+		 */
+		if (sjinfo->jointype == JOIN_LEFT && !sjinfo->inner_unique)
+			sjinfo->inner_unique = specialjoin_is_unique_join(root, sjinfo);
+	}
+}
 
 
 /*
@@ -95,6 +127,15 @@ restart:
 		root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo);
 
 		/*
+		 * mark_unique_joins may have been previously unable to mark a join as
+		 * unique due to there being multiple relations on the RHS of the
+		 * join. We may have just removed a relation so that there's now just
+		 * a singleton relation, so let's try again to mark the joins as
+		 * unique.
+		 */
+		mark_unique_joins(root);
+
+		/*
 		 * Restart the scan.  This is necessary to ensure we find all
 		 * removable joins independently of ordering of the join_info_list
 		 * (note that removal of attr_needed bits may make a join appear
@@ -156,31 +197,22 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	int			innerrelid;
 	RelOptInfo *innerrel;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
-	ListCell   *l;
 	int			attroff;
+	ListCell   *l;
 
 	/*
-	 * Must be a non-delaying left join to a single baserel, else we aren't
-	 * going to be able to do anything with it.
+	 * Join must have a unique inner side and must be a non-delaying join to a
+	 * single baserel, else we aren't going to be able to do anything with it.
 	 */
-	if (sjinfo->jointype != JOIN_LEFT ||
+	if (!sjinfo->inner_unique ||
 		sjinfo->delay_upper_joins)
 		return false;
 
 	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
-		return false;
+		return false;			/* should not happen */
 
 	innerrel = find_base_rel(root, innerrelid);
 
-	/*
-	 * Before we go to the effort of checking whether any innerrel variables
-	 * are needed above the join, make a quick check to eliminate cases in
-	 * which we will surely be unable to prove uniqueness of the innerrel.
-	 */
-	if (!rel_supports_distinctness(root, innerrel))
-		return false;
-
 	/* Compute the relid set for the join we are considering */
 	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
 
@@ -190,7 +222,8 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 *
 	 * Note that this test only detects use of inner-rel attributes in higher
 	 * join conditions and the target list.  There might be such attributes in
-	 * pushed-down conditions at this join, too.  We check that case below.
+	 * pushed-down conditions at this join too, but in this case the join
+	 * would not have been marked as unique.
 	 *
 	 * As a micro-optimization, it seems better to start with max_attr and
 	 * count down rather than starting with min_attr and counting up, on the
@@ -231,6 +264,41 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	return true;
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proven that this special join can only ever match at
+ *		most one inner tuple for any single outer tuple. False is returned if
+ *		there's insufficient evidence to prove the join is unique.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* if there's more than one relation involved, then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	/*
+	 * Before we go to the effort of pulling out the join condition's columns,
+	 * make a quick check to eliminate cases in which we will surely be unable
+	 * to prove uniqueness of the innerrel.
+	 */
+	if (!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -238,9 +306,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	 * it's what we want.  The mergejoinability test also eliminates clauses
 	 * containing volatile functions, which we couldn't depend on.
 	 */
-	foreach(l, innerrel->joininfo)
+	foreach(lc, innerrel->joininfo)
 	{
-		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(l);
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
 
 		/*
 		 * If it's not a join clause for this outer join, we can't use it.
@@ -252,10 +320,11 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we
+			 * can't say we're unique.  (XXX this is confusing conditions for
+			 * join removability with conditions for uniqueness, and could
+			 * probably stand to be improved.  But for the moment, keep on
+			 * applying the stricter condition.)
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -847,3 +916,170 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' is unique based on the join
+ *	  condition in 'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist)
+{
+	int			innerrelid;
+	bool		unique = false;
+
+	/* left joins were already checked by mark_unique_joins */
+	if (jointype == JOIN_LEFT)
+		return sjinfo->inner_unique;
+
+	/*
+	 * Currently we're unable to prove uniqueness when there's more than one
+	 * relation on the inner side of the join.
+	 */
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/* check other valid join types which can be proven unique */
+	if (jointype == JOIN_INNER ||
+		jointype == JOIN_RIGHT ||
+		jointype == JOIN_FULL)
+	{
+		MemoryContext old_context;
+		ListCell   *lc;
+
+		/* can't prove uniqueness for joins with an empty restrictlist */
+		if (restrictlist == NIL)
+			return false;
+
+		/*
+		 * First let's query the unique and non-unique caches to see if we've
+		 * managed to prove that innerrel is unique for some subset of this
+		 * outerrel. We don't need an exact match, as if we have any extra
+		 * outerrels than were previously cached, then they can't make the
+		 * innerrel any less unique.
+		 */
+		foreach(lc, root->unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(unique_rels, outerrel->relids))
+				return true;		/* Success! */
+		}
+
+		/*
+		 * We may have previously determined that this outerrel, or some
+		 * superset thereof, cannot prove this innerrel to be unique.
+		 */
+		foreach(lc, root->non_unique_rels[innerrelid])
+		{
+			Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+			if (bms_is_subset(outerrel->relids, unique_rels))
+				return false;
+		}
+
+		/* No cached information, so try to make the proof. */
+		if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+		{
+			unique = true;		/* Success! */
+
+			/*
+			 * Cache the positive result for future probes, being sure to keep
+			 * it in the planner_cxt even if we are working in GEQO.
+			 *
+			 * Note: one might consider trying to isolate the minimal subset
+			 * of the outerrels that proved the innerrel unique.  But it's not
+			 * worth the trouble, because the planner builds up joinrels
+			 * incrementally and so we'll see the minimally sufficient
+			 * outerrels before any supersets of them anyway.
+			 */
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->unique_rels[innerrelid] =
+				lappend(root->unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+		else
+		{
+			/*
+			 * None of the join conditions for outerrel proved innerrel
+			 * unique, so we can safely reject this outerrel or any subset of
+			 * it in future checks.
+			 *
+			 * However, in normal planning mode, caching this knowledge is
+			 * totally pointless; it won't be queried again, because we build
+			 * up joinrels from smaller to larger.  It is useful in GEQO mode,
+			 * where the knowledge can be carried across successive planning
+			 * attempts; and it's likely to be useful when using join-search
+			 * plugins, too.  Hence cache only when join_search_private is
+			 * non-NULL.  (Yeah, that's a hack, but it seems reasonable.)
+			 */
+			if (root->join_search_private)
+			{
+				old_context = MemoryContextSwitchTo(root->planner_cxt);
+				root->non_unique_rels[innerrelid] =
+					lappend(root->non_unique_rels[innerrelid],
+							bms_copy(outerrel->relids));
+				MemoryContextSwitchTo(old_context);
+			}
+		}
+	}
+	return unique;
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index fae1f67..c60c2c9 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -206,12 +206,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -226,7 +226,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3532,7 +3532,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -3835,7 +3835,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -3975,7 +3975,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5112,7 +5112,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5121,9 +5121,10 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5135,7 +5136,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5145,8 +5146,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5187,7 +5189,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5201,8 +5203,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index c170e96..5993d0b 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -1204,6 +1204,8 @@ make_outerjoininfo(PlannerInfo *root,
 	sjinfo->jointype = jointype;
 	/* this always starts out false */
 	sjinfo->delay_upper_joins = false;
+	/* this may be changed later */
+	sjinfo->inner_unique = false;
 
 	compute_semijoin_info(sjinfo, clause);
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index e880759..46dbe76 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
@@ -185,6 +188,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	fix_placeholder_input_needed_levels(root);
 
+	/* check for unique properties on outer joins */
+	mark_unique_joins(root);
+
 	/*
 	 * Remove any useless outer joins.  Ideally this would be done during
 	 * jointree preprocessing, but the necessary information isn't available
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index f440875..ebb0e40 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1935,11 +1935,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -1950,16 +1948,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -1995,7 +1992,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2008,8 +2005,9 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2022,10 +2020,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2038,10 +2035,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2049,6 +2045,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2058,7 +2055,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2071,12 +2068,13 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2088,11 +2086,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2102,15 +2098,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2120,7 +2115,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2145,10 +2140,11 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index adc1db9..d1bda16 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -81,6 +81,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f9bcdd6..a3f936d 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1669,6 +1669,10 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		match_first_inner_tuple_only;	/* True if we can safely move
+												 * to the next outer tuple
+												 * after matching first inner
+												 * tuple.*/
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index f72f7a8..6c2bc35 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -603,6 +603,8 @@ typedef struct CustomScan
  * jointype:	rule for joining tuples from left and right subtrees
  * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
  *				(plan.qual contains conditions that came from WHERE)
+ * inner_unique	outer tuples can only match to no more than one inner
+ *				tuple
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -618,6 +620,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 643be54..e195a7c 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -222,6 +222,18 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner side based on the join condition.
+	 * This is a rather expensive test to perform, and requires looking fo
+	 * proofs such as a relation's unique indexes.  We use this cache during
+	 * the join search to record lists of the sets of relations which both
+	 * prove, and disprove the uniqueness properties for the relid indexed by
+	 * these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1217,6 +1229,9 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* outer side of join matches no more than one
+								 * inner side tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -1810,6 +1825,8 @@ typedef struct SpecialJoinInfo
 	JoinType	jointype;		/* always INNER, LEFT, FULL, SEMI, or ANTI */
 	bool		lhs_strict;		/* joinclause is strict for some LHS rel */
 	bool		delay_upper_joins;		/* can't commute with upper RHS */
+	bool		inner_unique;	/* outer side of join matches no more than one
+								 * inner side tuple */
 	/* Remaining fields are set only for JOIN_SEMI jointype: */
 	bool		semi_can_btree; /* true if semi_operators are all btree */
 	bool		semi_can_hash;	/* true if semi_operators are all hash */
@@ -2042,6 +2059,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique true if proofs exist which prove that outer side tuples match
+ *		no more than one inner side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2050,6 +2069,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 39376ec..32561bc 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -122,33 +122,29 @@ extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void initial_cost_mergejoin(PlannerInfo *root,
 					   JoinCostWorkspace *workspace,
 					   JoinType jointype,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo);
+					   JoinPathExtraData *extra);
 extern void final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo);
+					 JoinPathExtraData *extra);
 extern void initial_cost_hashjoin(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void cost_gather(GatherPath *path, PlannerInfo *root,
 			RelOptInfo *baserel, ParamPathInfo *param_info, double *rows);
 extern void cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 7b41317..5ce410c 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -102,11 +102,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -114,10 +112,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -128,11 +125,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					  JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -238,6 +233,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..128753a 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -102,10 +102,15 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 /*
  * prototypes for plan/analyzejoins.c
  */
+extern void mark_unique_joins(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
-
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist);
 /*
  * prototypes for plan/setrefs.c
  */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..9784a87 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,7 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +391,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -982,29 +983,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 564218b..a96b2a1 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -317,12 +317,11 @@ explain (costs off)
                      ->  Index Scan using ec1_expr2 on ec1 ec1_1
                      ->  Index Scan using ec1_expr3 on ec1 ec1_2
                      ->  Index Scan using ec1_expr4 on ec1 ec1_3
-               ->  Materialize
-                     ->  Sort
-                           Sort Key: ec1.f1 USING <
-                           ->  Index Scan using ec1_pkey on ec1
-                                 Index Cond: (ff = '42'::bigint)
-(20 rows)
+               ->  Sort
+                     Sort Key: ec1.f1 USING <
+                     ->  Index Scan using ec1_pkey on ec1
+                           Index Cond: (ff = '42'::bigint)
+(19 rows)
 
 -- check partially indexed scan
 set enable_nestloop = on;
@@ -374,12 +373,11 @@ explain (costs off)
                Sort Key: (((ec1_2.ff + 3) + 1))
                ->  Seq Scan on ec1 ec1_2
          ->  Index Scan using ec1_expr4 on ec1 ec1_3
-   ->  Materialize
-         ->  Sort
-               Sort Key: ec1.f1 USING <
-               ->  Index Scan using ec1_pkey on ec1
-                     Index Cond: (ff = '42'::bigint)
-(14 rows)
+   ->  Sort
+         Sort Key: ec1.f1 USING <
+         ->  Index Scan using ec1_pkey on ec1
+               Index Cond: (ff = '42'::bigint)
+(13 rows)
 
 -- check effects of row-level security
 set enable_nestloop = on;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index d9bbae0..1512dd1 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3370,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3414,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3426,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3446,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3479,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3491,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3516,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3550,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3562,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3590,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3622,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3642,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3672,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3688,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3713,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3736,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3762,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3778,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3793,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3823,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3850,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3938,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3951,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -4801,12 +4834,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4833,12 +4867,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4866,6 +4901,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4873,7 +4909,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4893,12 +4929,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4920,10 +4957,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4932,7 +4971,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4978,16 +5017,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5054,13 +5095,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5076,7 +5121,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5094,17 +5139,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5126,7 +5176,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5139,16 +5189,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5161,10 +5213,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5173,7 +5227,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5200,10 +5254,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5225,7 +5281,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5323,3 +5379,248 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 79513e4..d7d2376 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,13 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(8 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 56481de..ff9cc77 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2010,12 +2010,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2036,11 +2037,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 47afdc3..071daf5 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -814,6 +814,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -822,7 +823,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -842,6 +843,7 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -859,7 +861,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 3b7f689..3e4c3fb 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2195,6 +2195,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2202,6 +2203,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2209,6 +2211,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2216,12 +2219,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 97bccec..39fbda1 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1730,3 +1730,94 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+drop table j1;
+drop table j2;
+drop table j3;
#100Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#99)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

On 27 January 2017 at 12:39, Tom Lane <tgl@sss.pgh.pa.us> wrote:

2. In these same cases (unique/semi/anti joins), it is possible to avoid
mark/restore overhead in a mergejoin, because we can tweak the executor
logic to not require backing up the inner side.

I've made modifications in the attached to add this optimization, and
it's quite a significant improvement.

Cool ... validates my gut feeling that that was worth incorporating.

... IOW, would it hurt to drop the SpecialJoinInfo
tie-in and just rely on the generic cache?

I agree that special handling of one join type is not so pretty.
However, LEFT JOINs still remain a bit special as they're the only
ones we currently perform join removal on, and the patch modifies that
code to make use of the new flag for those. This can improve planner
performance of join removal when a join is removed successfully, as
the previous code had to recheck uniqueness of each remaining LEFT
JOIN again, whereas the new code only checks uniqueness of ones not
previously marked as unique. This too likely could be done with the
cache, although I'm a bit concerned with populating the cache, then
performing a bunch of LEFT JOIN removals and leaving relids in the
cache which no longer exist. Perhaps it's OK. I've just not found
proofs in my head yet that it is.

TBH, I do not like that tie-in at all. I don't believe that it improves
any performance, because if analyzejoins.c detects that the join is
unique, it will remove the join; therefore there is nothing to cache.
(This statement depends on the uniqueness test being the last removability
test, but it is.) And running mark_unique_joins() multiple times is ugly
and adds cycles whenever it cannot prove a join unique, because it'll keep
trying to do so. So I'm pretty inclined to drop the connection to
analyzejoins.c altogether, along with mark_unique_joins(), and just use
the generic positive/negative cache mechanism you added for all join types.

It's possible that it'd make sense for analyzejoins.c to add a negative
cache entry about any join that it tries and fails to prove unique.
Your point about cache maintenance is valid, but a simple answer there
would just be to flush the cache whenever we remove a rel. (Although
I'm dubious that we need to: how could a removable rel be part of the
min outer rels for any surviving rel?)

* Because of where we apply the short-circuit logic in the executor,
it's only safe to consider the inner rel as unique if it is provably
unique using only the join clauses that drive the primary join mechanism
(ie, the "joinquals" not the "otherquals"). We already do ignore quals
that are pushed-down to an outer join, so that's good, but there's an
oversight: we will use a qual that is mergejoinable even if it's not
hashjoinable. That means we could get the wrong answers in a hash join.

hmm. I'm having trouble understanding why this is a problem for Unique
joins, but not for join removal?

Ah, you know what, that's just mistaken. I was thinking that we
short-circuited the join on the strength of the hash (or merge) quals
only, but actually we check all the joinquals first. As long as the
uniqueness proof uses only joinquals and not conditions that will end up
as otherquals, it's fine.

However you mentioning this cause me to notice that this is only true
in the patch for left joins, and the other join types are not
consistent with that.

Hm, perhaps, but the example you show isn't proving that, because it's
choosing to put a1 on the inside in the innerjoin case, and a1 certainly
isn't unique for this query. We can't see whether a2 was detected as
unique.

regards, tom lane

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

#101Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#100)
Re: Performance improvement for joins where outer side is unique

I wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

hmm. I'm having trouble understanding why this is a problem for Unique
joins, but not for join removal?

Ah, you know what, that's just mistaken. I was thinking that we
short-circuited the join on the strength of the hash (or merge) quals
only, but actually we check all the joinquals first. As long as the
uniqueness proof uses only joinquals and not conditions that will end up
as otherquals, it's fine.

Actually, after thinking about that some more, it seems to me that there
is a performance (not correctness) issue here: suppose that we have
something like

select ... from t1 left join t2 on t1.x = t2.x and t1.y < t2.y

If there's a unique index on t2.x, we'll be able to mark the join
inner-unique. However, short-circuiting would only occur after
finding a row that passes both joinquals. If the y condition is
true for only a few rows, this would pretty nearly disable the
optimization. Ideally we would short-circuit after testing the x
condition only, but there's no provision for that.

This might not be a huge problem for outer joins. My sense of typical
SQL style is that the joinquals (ON conditions) are likely to be
exactly what you need to prove inner uniqueness, while random other
conditions will be pushed-down from WHERE and hence will be otherquals.
But I'm afraid it is quite a big deal for inner joins, where we dump
all available conditions into the joinquals. We might need to rethink
that choice.

At least for merge and hash joins, it's tempting to think about a
short-circuit test being made after testing just the merge/hash quals.
But we'd have to prove uniqueness using only the merge/hash quals,
so the planning cost might be unacceptably high --- particularly for
merge joins which often don't use all available mergeable quals.
In the end I think we probably want to keep the short-circuit in the
same place where it is for SEMI/ANTI cases (which have to have it
exactly there for semantic correctness).

I'm afraid though that we may have to do something about the
irrelevant-joinquals issue in order for this to be of much real-world
use for inner joins.

regards, tom lane

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

#102Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#101)
Re: Performance improvement for joins where outer side is unique

On Fri, Jan 27, 2017 at 11:44 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I'm afraid though that we may have to do something about the
irrelevant-joinquals issue in order for this to be of much real-world
use for inner joins.

Maybe, but it's certainly not the case that all inner joins are highly
selective. There are plenty of inner joins in real-world applications
where the join product is 10% or 20% or 50% of the size of the larger
input.

--
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

#103Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#102)
Re: Performance improvement for joins where outer side is unique

Robert Haas <robertmhaas@gmail.com> writes:

On Fri, Jan 27, 2017 at 11:44 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I'm afraid though that we may have to do something about the
irrelevant-joinquals issue in order for this to be of much real-world
use for inner joins.

Maybe, but it's certainly not the case that all inner joins are highly
selective. There are plenty of inner joins in real-world applications
where the join product is 10% or 20% or 50% of the size of the larger
input.

Um ... what's that got to do with the point at hand?

regards, tom lane

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

#104Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#103)
Re: Performance improvement for joins where outer side is unique

On Fri, Jan 27, 2017 at 1:39 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

On Fri, Jan 27, 2017 at 11:44 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I'm afraid though that we may have to do something about the
irrelevant-joinquals issue in order for this to be of much real-world
use for inner joins.

Maybe, but it's certainly not the case that all inner joins are highly
selective. There are plenty of inner joins in real-world applications
where the join product is 10% or 20% or 50% of the size of the larger
input.

Um ... what's that got to do with the point at hand?

I thought it was directly relevant, but maybe I'm confused. Further
up in that email, you wrote: "If there's a unique index on t2.x, we'll
be able to mark the join inner-unique. However, short-circuiting
would only occur after finding a row that passes both joinquals. If
the y condition is true for only a few rows, this would pretty nearly
disable the optimization. Ideally we would short-circuit after
testing the x condition only, but there's no provision for that."

So I assumed from that that the issue was that you'd have to wait for
the first time the irrelevant-joinqual got satisfied before the
optimization kicked in. But, if the join is emitting lots of rows,
that'll happen pretty quickly. I mean, if the join emits even as many
20 rows, the time after the first one is, all things being equal, 95%
of the runtime of the join. There could certainly be bad cases where
it takes a long time to produce the first row, but I wouldn't say
that's a particularly common thing.

--
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

#105Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#104)
Re: Performance improvement for joins where outer side is unique

Robert Haas <robertmhaas@gmail.com> writes:

On Fri, Jan 27, 2017 at 1:39 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Um ... what's that got to do with the point at hand?

So I assumed from that that the issue was that you'd have to wait for
the first time the irrelevant-joinqual got satisfied before the
optimization kicked in.

No, the problem is that that needs to happen for *each* outer row,
and there's only one chance for it to happen. Given the previous
example,

select ... from t1 left join t2 on t1.x = t2.x and t1.y < t2.y

once we've found an x match for a given outer row, there aren't going to
be any more and we should move on to the next outer row. But as the patch
stands, we only recognize that if t1.y < t2.y happens to be true for that
particular row pair. Otherwise we'll keep searching and we'll never find
another match for that outer row. So if the y condition is, say, 50%
selective then the optimization only wins for 50% of the outer rows
(that have an x partner in the first place).

Now certainly that's better than a sharp stick in the eye, and
maybe we should just press forward anyway. But it feels like
this is leaving a lot more on the table than I originally thought.
Especially for the inner-join case, where *all* the WHERE conditions
get a chance to break the optimization this way.

regards, tom lane

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

#106Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#105)
Re: Performance improvement for joins where outer side is unique

On Fri, Jan 27, 2017 at 2:00 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

On Fri, Jan 27, 2017 at 1:39 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Um ... what's that got to do with the point at hand?

So I assumed from that that the issue was that you'd have to wait for
the first time the irrelevant-joinqual got satisfied before the
optimization kicked in.

No, the problem is that that needs to happen for *each* outer row,
and there's only one chance for it to happen. Given the previous
example,

select ... from t1 left join t2 on t1.x = t2.x and t1.y < t2.y

once we've found an x match for a given outer row, there aren't going to
be any more and we should move on to the next outer row. But as the patch
stands, we only recognize that if t1.y < t2.y happens to be true for that
particular row pair. Otherwise we'll keep searching and we'll never find
another match for that outer row. So if the y condition is, say, 50%
selective then the optimization only wins for 50% of the outer rows
(that have an x partner in the first place).

Now certainly that's better than a sharp stick in the eye, and
maybe we should just press forward anyway. But it feels like
this is leaving a lot more on the table than I originally thought.
Especially for the inner-join case, where *all* the WHERE conditions
get a chance to break the optimization this way.

OK, now I understand why you were concerned. Given the size of some of
the speedups David's reported on this thread, I'd be tempted to press
forward even if no solution to this part of the problem presents
itself, but I also agree with you that it's leaving quite a bit on the
table if we can't do better.

--
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

#107David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#100)
Re: Performance improvement for joins where outer side is unique

On 28 January 2017 at 05:04, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

I agree that special handling of one join type is not so pretty.
However, LEFT JOINs still remain a bit special as they're the only
ones we currently perform join removal on, and the patch modifies that
code to make use of the new flag for those. This can improve planner
performance of join removal when a join is removed successfully, as
the previous code had to recheck uniqueness of each remaining LEFT
JOIN again, whereas the new code only checks uniqueness of ones not
previously marked as unique. This too likely could be done with the
cache, although I'm a bit concerned with populating the cache, then
performing a bunch of LEFT JOIN removals and leaving relids in the
cache which no longer exist. Perhaps it's OK. I've just not found
proofs in my head yet that it is.

TBH, I do not like that tie-in at all. I don't believe that it improves
any performance, because if analyzejoins.c detects that the join is
unique, it will remove the join; therefore there is nothing to cache.
(This statement depends on the uniqueness test being the last removability
test, but it is.) And running mark_unique_joins() multiple times is ugly
and adds cycles whenever it cannot prove a join unique, because it'll keep
trying to do so. So I'm pretty inclined to drop the connection to
analyzejoins.c altogether, along with mark_unique_joins(), and just use
the generic positive/negative cache mechanism you added for all join types.

I can make this change, but before I do I just want to point that I
don't think what you've said here is entirely accurate.

Let's assume unique joins are very common place, and join removals are
not so common. If a query has 5 left joins, and only one of which can
be removed, then the new code will most likely perform 5 unique join
checks, whereas the old code would perform 9, as those unique checks
are performed again once the 1 relation is removed for the remaining
4.

However I'll go make the change as something needs fixed in that area
anyway, as LEFT JOINs use the additional quals, whereas other join
types don't, which is broken.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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

#108Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#107)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

I can make this change, but before I do I just want to point that I
don't think what you've said here is entirely accurate.

Let's assume unique joins are very common place, and join removals are
not so common. If a query has 5 left joins, and only one of which can
be removed, then the new code will most likely perform 5 unique join
checks, whereas the old code would perform 9, as those unique checks
are performed again once the 1 relation is removed for the remaining
4.

I'm not following. If the join removal code had reached the stage of
making a uniqueness check, and that check had succeeded, the join would be
gone and there would be no repeat check later. If it didn't reach the
stage of making a uniqueness check, then again there's no duplication.
There will be some advantage in making a negative cache entry if join
removal performs a uniqueness check that fails, but I don't really see
why that's hard. It does not seem like removal of a relation could
cause another rel to become unique that wasn't before, so keeping
negative cache entries across join removals ought to be safe.

regards, tom lane

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

#109David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#108)
Re: Performance improvement for joins where outer side is unique

On 31 January 2017 at 04:56, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

I can make this change, but before I do I just want to point that I
don't think what you've said here is entirely accurate.

Let's assume unique joins are very common place, and join removals are
not so common. If a query has 5 left joins, and only one of which can
be removed, then the new code will most likely perform 5 unique join
checks, whereas the old code would perform 9, as those unique checks
are performed again once the 1 relation is removed for the remaining
4.

I'm not following. If the join removal code had reached the stage of
making a uniqueness check, and that check had succeeded, the join would be
gone and there would be no repeat check later. If it didn't reach the
stage of making a uniqueness check, then again there's no duplication.

I had forgotten the unique check was performed last. In that case the
check for unused columns is duplicated needlessly each time. But let's
drop it, as putting the code back is not making things any worse.

There will be some advantage in making a negative cache entry if join
removal performs a uniqueness check that fails, but I don't really see
why that's hard. It does not seem like removal of a relation could
cause another rel to become unique that wasn't before, so keeping
negative cache entries across join removals ought to be safe.

I don't think that's possible. The whole point that the current join
removal code retries to remove joins which it already tried to remove,
after a successful removal is exactly because it is possible for a
join to become provability unique on the removal of another join. If
you remove that retry code, a regression test fails. I believe this is
because there initially would have been more than one RHS rel, and the
bitmap singleton check would have failed. After all a unique index is
on a single relation, so proofs don't exist when >1 rel is on the RHS.

In any case, it's not possible to use the cache with join removals, as
we use the otherquals for unique tests in join removals, but we can't
for unique joins, as I'm adding those optimizations to the executor
which rely on the uniqueness being on the join condition alone, so we
can skip to the next outer tuple on matched join, but unmatched quals.
If I change how join removals work in that regard it will disallow
join removals where they were previously possible. So that's a no go
area.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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

#110Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#109)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

On 31 January 2017 at 04:56, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I'm not following. If the join removal code had reached the stage of
making a uniqueness check, and that check had succeeded, the join would be
gone and there would be no repeat check later. If it didn't reach the
stage of making a uniqueness check, then again there's no duplication.

I had forgotten the unique check was performed last. In that case the
check for unused columns is duplicated needlessly each time.

I think we do need to repeat that each time, as columns that were formerly
used in a join condition to a now-dropped relation might thereby have
become unused.

But let's
drop it, as putting the code back is not making things any worse.

Agreed, if there is something to be won there, we can address it
separately.

I don't think that's possible. The whole point that the current join
removal code retries to remove joins which it already tried to remove,
after a successful removal is exactly because it is possible for a
join to become provability unique on the removal of another join.

Not seeing that ... example please?

If you remove that retry code, a regression test fails.

Probably because of the point about unused columns...

regards, tom lane

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

#111David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#101)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 28 January 2017 at 05:44, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

hmm. I'm having trouble understanding why this is a problem for Unique
joins, but not for join removal?

Ah, you know what, that's just mistaken. I was thinking that we
short-circuited the join on the strength of the hash (or merge) quals
only, but actually we check all the joinquals first. As long as the
uniqueness proof uses only joinquals and not conditions that will end up
as otherquals, it's fine.

Actually, after thinking about that some more, it seems to me that there
is a performance (not correctness) issue here: suppose that we have
something like

select ... from t1 left join t2 on t1.x = t2.x and t1.y < t2.y

If there's a unique index on t2.x, we'll be able to mark the join
inner-unique. However, short-circuiting would only occur after
finding a row that passes both joinquals. If the y condition is
true for only a few rows, this would pretty nearly disable the
optimization. Ideally we would short-circuit after testing the x
condition only, but there's no provision for that.

I've attached a patch which implements this, though only for
MergeJoin, else I'd imagine we'd also need to ensure all proofs used
for testing the uniqueness were also hash-able too. I added some XXX
comments in analyzejoin.c around the mergeopfamilies == NIL tests to
mention that Merge Join depends on all the unique proof quals having
mergeopfamilies. This also assumes we'll never use some subset of
mergejoin-able quals for a merge join, which could be an interesting
area in the future, as we might have some btree index on a subset of
those columns to provide pre-sorted input. In short, it's a great
optimisation, but I'm a little scared we might break it one day.

Implementing this meant removing the match_first_row_only being set
for JOIN_SEMI, as this optimisation only applies to unique_inner and
not JOIN_SEMI alone. This also means we should be checking for unique
properties on JOIN_SEMI now too, which I've enabled.

Also, I've removed all jointype checks from innerrel_is_unique(). I
took a bit of time to think about JOIN_UNIQUE_OUTER and
JOIN_UNIQUE_INNER. I think these are fine too, in fact one of the
regression test plans moved away from using a Semi Join due to proving
that the inner side was unique. That could perhaps allow a better
plan, since the join order can be swapped. I'd really like someone
else to have a think about that too, just to make sure I've not
blundered that.

I've put the join removal code back to the way it was before. As I
mentioned, we can't use the caches here since we're also using
additional quals for proofs in this case.

I wasn't sure if I should add some regression tests which exercises
MergeJoin a bit to test the new optimisation. Any thoughts on that?

David

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-01-31.patchapplication/octet-stream; name=unique_joins_2017-01-31.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 0a67be0..fc4c92a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1309,6 +1309,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+				ExplainPropertyText("Inner Unique", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 8a04294..f79f139 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -288,10 +288,11 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we to only match one
+					 * inner tuple, or if there's only one possible inner
+					 * matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI || node->js.inner_unique)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -402,6 +403,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.inner_unique = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 105e2dc..55a9799 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -802,10 +802,11 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we to only match one
+					 * inner tuple, or if there's only one possible inner
+					 * matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI || node->js.inner_unique)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -826,7 +827,20 @@ ExecMergeJoin(MergeJoinState *node)
 						InstrCountFiltered2(node, 1);
 				}
 				else
+				{
+					/*
+					 * If the inner side of the join is unique, then it's not
+					 * possible that we'll find another row matching this
+					 * outer row, so we can now move along to the next outer
+					 * row. We're unable to perform the same optimization for
+					 * SEMI_JOINs, as there might be another row matching the
+					 * join condition.
+					 */
+					if (node->js.inner_unique)
+						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
+
 					InstrCountFiltered1(node, 1);
+				}
 				break;
 
 				/*
@@ -1048,7 +1062,11 @@ ExecMergeJoin(MergeJoinState *node)
 					/*
 					 * the merge clause matched so now we restore the inner
 					 * scan position to the first mark, and go join that tuple
-					 * (and any following ones) to the new outer.
+					 * (and any following ones) to the new outer. In the case
+					 * where we've only joined to the first inner tuple, we've
+					 * not advanced the inner side any away from where the
+					 * restore point would normally be, so have no need to
+					 * perform the restore.
 					 *
 					 * NOTE: we do not need to worry about the MatchedInner
 					 * state for the rescanned inner tuples.  We know all of
@@ -1062,17 +1080,20 @@ ExecMergeJoin(MergeJoinState *node)
 					 * forcing the merge clause to never match, so we never
 					 * get here.
 					 */
-					ExecRestrPos(innerPlan);
+					if (node->js.jointype != JOIN_SEMI && !node->js.inner_unique)
+					{
+						ExecRestrPos(innerPlan);
 
-					/*
-					 * ExecRestrPos probably should give us back a new Slot,
-					 * but since it doesn't, use the marked slot.  (The
-					 * previously returned mj_InnerTupleSlot cannot be assumed
-					 * to hold the required tuple.)
-					 */
-					node->mj_InnerTupleSlot = innerTupleSlot;
-					/* we need not do MJEvalInnerValues again */
+						/*
+						 * ExecRestrPos probably should give us back a new
+						 * Slot, but since it doesn't, use the marked slot.
+						 * (The previously returned mj_InnerTupleSlot cannot
+						 * be assumed to hold the required tuple.)
+						 */
+						node->mj_InnerTupleSlot = innerTupleSlot;
+					}
 
+					/* we need not do MJEvalInnerValues again */
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
@@ -1172,9 +1193,17 @@ ExecMergeJoin(MergeJoinState *node)
 
 				if (compareResult == 0)
 				{
-					ExecMarkPos(innerPlan);
+					/*
+					 * When we're only going join to a single inner tuple then
+					 * we'll not advance the inner side further on the current
+					 * outer tuple, so have no need to perform a Mark.
+					 */
+					if (node->js.jointype != JOIN_SEMI && !node->js.inner_unique)
+					{
+						ExecMarkPos(innerPlan);
 
-					MarkInnerTuple(node->mj_InnerTupleSlot, node);
+						MarkInnerTuple(node->mj_InnerTupleSlot, node);
+					}
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
@@ -1438,6 +1467,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.inner_unique = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index cac7ba1..dd1c4ed 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -219,10 +219,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we to only match one inner
+			 * tuple, or if there's only one possible inner matching tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.jointype == JOIN_SEMI || node->js.inner_unique)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -273,6 +273,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.inner_unique = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index a43daa7..2afdfca 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1852,15 +1852,13 @@ cost_group(Path *path, PlannerInfo *root,
  * 'jointype' is the type of join to be performed
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -1891,10 +1889,13 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With unique inner, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -1927,17 +1928,16 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  *
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->outerjoinpath;
 	Path	   *inner_path = path->innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	Cost		startup_cost = workspace->startup_cost;
@@ -1972,10 +1972,13 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With unique inner, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2116,7 +2119,7 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
  * 'inner_path' is the inner input to the join
  * 'outersortkeys' is the list of sort keys for the outer path
  * 'innersortkeys' is the list of sort keys for the inner path
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  *
  * Note: outersortkeys and innersortkeys should be NIL if no explicit
  * sort is needed because the respective source path is already ordered.
@@ -2127,7 +2130,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo)
+					   JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2204,7 +2207,8 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerstartsel = cache->leftstartsel;
 			innerendsel = cache->leftendsel;
 		}
-		if (jointype == JOIN_LEFT ||
+		if (extra->inner_unique ||
+			jointype == JOIN_LEFT ||
 			jointype == JOIN_ANTI)
 		{
 			outerstartsel = 0.0;
@@ -2342,12 +2346,12 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		materialize_inner
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo)
+					 JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
@@ -2431,9 +2435,14 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * computations?
 	 *
 	 * The whole issue is moot if we are working from a unique-ified outer
-	 * input.
+	 * input. We'll also never rescan during SEMI/ANTI/inner_unique as there's
+	 * no need to advance the inner side to find subsequent matches, therefore
+	 * no need to mark/restore.
 	 */
-	if (IsA(outer_path, UniquePath))
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		IsA(outer_path, UniquePath))
 		rescannedtuples = 0;
 	else
 	{
@@ -2482,7 +2491,10 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	/*
 	 * Even if materializing doesn't look cheaper, we *must* do it if the
 	 * inner path is to be used directly (without sorting) and it doesn't
-	 * support mark/restore.
+	 * support mark/restore. However, if the inner side of the join is
+	 * guaranteed not to duplicate outer rows, as in the case with SEMI/ANTI
+	 * JOINs and joins which have an inner unique side, then we'll never need
+	 * to perform mark/restore.
 	 *
 	 * Since the inner side must be ordered, and only Sorts and IndexScans can
 	 * create order to begin with, and they both support mark/restore, you
@@ -2495,7 +2507,9 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * materialization is required for correctness in this case, and turning
 	 * it off does not entitle us to deliver an invalid plan.
 	 */
-	else if (innersortkeys == NIL &&
+	else if (innersortkeys == NIL && !extra->inner_unique &&
+			 path->jpath.jointype != JOIN_SEMI &&
+			 path->jpath.jointype != JOIN_ANTI &&
 			 !ExecSupportsMarkRestore(inner_path))
 		path->materialize_inner = true;
 
@@ -2637,16 +2651,14 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
  * 'hashclauses' is the list of joinclauses to be used as hash clauses
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2731,17 +2743,16 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	List	   *hashclauses = path->path_hashclauses;
@@ -2859,13 +2870,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With unique inner, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 7c30ec6..6367a2c 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -19,6 +19,7 @@
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "optimizer/cost.h"
+#include "optimizer/planmain.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 
@@ -103,6 +104,13 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.
+	 */
+	extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+											jointype, sjinfo, restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -318,8 +326,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * methodology worthwhile.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -330,11 +337,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -381,8 +386,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
 
@@ -392,11 +396,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -452,7 +454,7 @@ try_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -463,10 +465,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -516,8 +517,7 @@ try_hashjoin_path(PlannerInfo *root,
 	 * never have any output pathkeys, per comments in create_hashjoin_path.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -528,11 +528,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -579,8 +577,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, NIL))
 		return;
 
@@ -590,11 +587,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 438baf1..1bba520 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -34,6 +34,8 @@
 
 /* local functions */
 static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo);
+static bool specialjoin_is_unique_join(PlannerInfo *root,
+						   SpecialJoinInfo *sjinfo);
 static void remove_rel_from_query(PlannerInfo *root, int relid,
 					  Relids joinrelids);
 static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved);
@@ -41,6 +43,10 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
 
 
 /*
@@ -156,7 +162,6 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 	int			innerrelid;
 	RelOptInfo *innerrel;
 	Relids		joinrelids;
-	List	   *clause_list = NIL;
 	ListCell   *l;
 	int			attroff;
 
@@ -231,6 +236,37 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			return false;		/* it does reference innerrel */
 	}
 
+	/* Analyse the join to see if there's any valid unique proofs */
+	return  specialjoin_is_unique_join(root, sjinfo);
+}
+
+/*
+ * specialjoin_is_unique_join
+ *		True if it can be proven that this special join can only ever match at
+ *		most one inner tuple for any single outer tuple. False is returned if
+ *		there's insufficient evidence to prove the join is unique.
+ *
+ * Calling funtions may like to perform a prior check on
+ * rel_supports_distinctness() before calling this function.
+ */
+static bool
+specialjoin_is_unique_join(PlannerInfo *root, SpecialJoinInfo *sjinfo)
+{
+	int			innerrelid;
+	RelOptInfo *innerrel;
+	Relids		joinrelids;
+	List	   *clause_list = NIL;
+	ListCell   *l;
+
+	/* if there's more than one relation involved, then punt */
+	if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid))
+		return false;
+
+	innerrel = find_base_rel(root, innerrelid);
+
+	/* Compute the relid set for the join we are considering */
+	joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand);
+
 	/*
 	 * Search for mergejoinable clauses that constrain the inner rel against
 	 * either the outer rel or a pseudoconstant.  If an operator is
@@ -252,10 +288,11 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo)
 			!bms_equal(restrictinfo->required_relids, joinrelids))
 		{
 			/*
-			 * If such a clause actually references the inner rel then join
-			 * removal has to be disallowed.  We have to check this despite
-			 * the previous attr_needed checks because of the possibility of
-			 * pushed-down clauses referencing the rel.
+			 * If such a clause actually references the inner rel then we
+			 * can't say we're unique.  (XXX this is confusing conditions for
+			 * join removability with conditions for uniqueness, and could
+			 * probably stand to be improved.  But for the moment, keep on
+			 * applying the stricter condition.)
 			 */
 			if (bms_is_member(innerrelid, restrictinfo->clause_relids))
 				return false;
@@ -847,3 +884,171 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' is unique based on the join
+ *	  condition in 'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist)
+{
+	MemoryContext	old_context;
+	ListCell	   *lc;
+	int				innerrelid;
+	bool			unique = false;
+
+	/* If join type is not supported, then bail. Note that it's useful to also
+	 * mark SEMI/ANTI joins as unique as. XXX do we need to bother checking
+	 * the join type here?
+	*/
+	if (jointype != JOIN_INNER &&
+		jointype != JOIN_LEFT &&
+		jointype != JOIN_RIGHT &&
+		jointype != JOIN_SEMI &&
+		jointype != JOIN_ANTI &&
+		jointype != JOIN_FULL)
+		return false;
+
+	/* can't prove uniqueness for joins with an empty restrictlist */
+	if (restrictlist == NIL)
+		return false;
+
+	/*
+	 * Currently we're unable to prove uniqueness when there's more than one
+	 * relation on the inner side of the join.
+	 */
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/*
+	 * First let's query the unique and non-unique caches to see if we've
+	 * managed to prove that innerrel is unique for some subset of this
+	 * outerrel. We don't need an exact match, as if we have any extra
+	 * outerrels than were previously cached, then they can't make the
+	 * innerrel any less unique.
+	 */
+	foreach(lc, root->unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(unique_rels, outerrel->relids))
+			return true;		/* Success! */
+	}
+
+	/*
+	 * We may have previously determined that this outerrel, or some superset
+	 * thereof, cannot prove this innerrel to be unique.
+	 */
+	foreach(lc, root->non_unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(outerrel->relids, unique_rels))
+			return false;
+	}
+
+	/* No cached information, so try to make the proof. */
+	if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+	{
+		unique = true;		/* Success! */
+
+		/*
+		 * Cache the positive result for future probes, being sure to keep it
+		 * in the planner_cxt even if we are working in GEQO.
+		 *
+		 * Note: one might consider trying to isolate the minimal subset of
+		 * the outerrels that proved the innerrel unique.  But it's not worth
+		 * the trouble, because the planner builds up joinrels incrementally
+		 * and so we'll see the minimally sufficient outerrels before any
+		 * supersets of them anyway.
+		 */
+		old_context = MemoryContextSwitchTo(root->planner_cxt);
+		root->unique_rels[innerrelid] =
+			lappend(root->unique_rels[innerrelid],
+					bms_copy(outerrel->relids));
+		MemoryContextSwitchTo(old_context);
+	}
+	else
+	{
+		/*
+		 * None of the join conditions for outerrel proved innerrel unique, so
+		 * we can safely reject this outerrel or any subset of it in future
+		 * checks.
+		 *
+		 * However, in normal planning mode, caching this knowledge is totally
+		 * pointless; it won't be queried again, because we build up joinrels
+		 * from smaller to larger.  It is useful in GEQO mode, where the
+		 * knowledge can be carried across successive planning attempts; and
+		 * it's likely to be useful when using join-search plugins, too.
+		 * Hence cache only when join_search_private is non-NULL.  (Yeah,
+		 * that's a hack, but it seems reasonable.)
+		 */
+		if (root->join_search_private)
+		{
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->non_unique_rels[innerrelid] =
+				lappend(root->non_unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+	}
+	return unique;
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index fae1f67..c60c2c9 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -206,12 +206,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -226,7 +226,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3532,7 +3532,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -3835,7 +3835,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -3975,7 +3975,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5112,7 +5112,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5121,9 +5121,10 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5135,7 +5136,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5145,8 +5146,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5187,7 +5189,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5201,8 +5203,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index e880759..4a36b82 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index f440875..ebb0e40 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1935,11 +1935,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -1950,16 +1948,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -1995,7 +1992,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2008,8 +2005,9 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2022,10 +2020,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2038,10 +2035,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2049,6 +2045,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2058,7 +2055,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2071,12 +2068,13 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2088,11 +2086,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2102,15 +2098,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2120,7 +2115,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2145,10 +2140,11 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index adc1db9..d1bda16 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -81,6 +81,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f9bcdd6..91e97d8 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1669,6 +1669,8 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		inner_unique;	/* True if each outer tuple can match to no
+								 * more than one inner tuple.*/
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index f72f7a8..f2ebee2 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -603,6 +603,8 @@ typedef struct CustomScan
  * jointype:	rule for joining tuples from left and right subtrees
  * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
  *				(plan.qual contains conditions that came from WHERE)
+ * inner_unique	each outer tuple can match to no more than one inner
+ *				tuple
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -618,6 +620,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 643be54..1b81579 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -222,6 +222,18 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner side based on the join condition.
+	 * This is a rather expensive test to perform, and requires looking for
+	 * proofs such as a relation's unique indexes.  We use this cache during
+	 * the join search to record lists of the sets of relations which both
+	 * prove, and disprove the uniqueness properties for the relid indexed by
+	 * these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1217,6 +1229,9 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* each outer tuple can match to no more than
+								 * one inner tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -2042,6 +2057,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique true if proofs exist which prove that outer side tuples match
+ *		no more than one inner side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2050,6 +2067,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 0e68264..6f22bcd 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -122,33 +122,29 @@ extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void initial_cost_mergejoin(PlannerInfo *root,
 					   JoinCostWorkspace *workspace,
 					   JoinType jointype,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo);
+					   JoinPathExtraData *extra);
 extern void final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo);
+					 JoinPathExtraData *extra);
 extern void initial_cost_hashjoin(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void cost_gather(GatherPath *path, PlannerInfo *root,
 			RelOptInfo *baserel, ParamPathInfo *param_info, double *rows);
 extern void cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 7b41317..5ce410c 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -102,11 +102,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -114,10 +112,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -128,11 +125,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					  JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -238,6 +233,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..e37c647 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -105,7 +105,11 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
-
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist);
 /*
  * prototypes for plan/setrefs.c
  */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..9784a87 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,7 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +391,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -982,29 +983,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 564218b..a96b2a1 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -317,12 +317,11 @@ explain (costs off)
                      ->  Index Scan using ec1_expr2 on ec1 ec1_1
                      ->  Index Scan using ec1_expr3 on ec1 ec1_2
                      ->  Index Scan using ec1_expr4 on ec1 ec1_3
-               ->  Materialize
-                     ->  Sort
-                           Sort Key: ec1.f1 USING <
-                           ->  Index Scan using ec1_pkey on ec1
-                                 Index Cond: (ff = '42'::bigint)
-(20 rows)
+               ->  Sort
+                     Sort Key: ec1.f1 USING <
+                     ->  Index Scan using ec1_pkey on ec1
+                           Index Cond: (ff = '42'::bigint)
+(19 rows)
 
 -- check partially indexed scan
 set enable_nestloop = on;
@@ -374,12 +373,11 @@ explain (costs off)
                Sort Key: (((ec1_2.ff + 3) + 1))
                ->  Seq Scan on ec1 ec1_2
          ->  Index Scan using ec1_expr4 on ec1 ec1_3
-   ->  Materialize
-         ->  Sort
-               Sort Key: ec1.f1 USING <
-               ->  Index Scan using ec1_pkey on ec1
-                     Index Cond: (ff = '42'::bigint)
-(14 rows)
+   ->  Sort
+         Sort Key: ec1.f1 USING <
+         ->  Index Scan using ec1_pkey on ec1
+               Index Cond: (ff = '42'::bigint)
+(13 rows)
 
 -- check effects of row-level security
 set enable_nestloop = on;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index d9bbae0..6782d00 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3370,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3414,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3426,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3446,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3479,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3491,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3516,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3550,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3562,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3590,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3622,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3642,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3672,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3688,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3713,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3736,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3762,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3778,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3793,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3823,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3850,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3938,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3951,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -4801,12 +4834,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4833,12 +4867,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4866,6 +4901,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4873,7 +4909,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4893,12 +4929,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4920,10 +4957,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4932,7 +4971,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4978,16 +5017,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5054,13 +5095,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5076,7 +5121,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5094,17 +5139,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5126,7 +5176,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5139,16 +5189,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5161,10 +5213,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5173,7 +5227,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5200,10 +5254,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5225,7 +5281,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5323,3 +5379,283 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop Left Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 79513e4..d7d2376 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,13 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(8 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 56481de..ff9cc77 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2010,12 +2010,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2036,11 +2037,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 47afdc3..071daf5 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -814,6 +814,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -822,7 +823,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -842,6 +843,7 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -859,7 +861,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 3b7f689..3e4c3fb 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2195,6 +2195,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2202,6 +2203,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2209,6 +2211,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2216,12 +2219,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 97bccec..a39ae9f 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1730,3 +1730,106 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+
+drop table j1;
+drop table j2;
+drop table j3;
#112David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#111)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 31 January 2017 at 13:10, David Rowley <david.rowley@2ndquadrant.com> wrote:

I've attached a patch which implements this.

Please disregards previous patch. (I forgot git commit before git diff
to make the patch)

I've attached the correct patch.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-01-31a.patchapplication/octet-stream; name=unique_joins_2017-01-31a.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 0a67be0..fc4c92a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1309,6 +1309,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+				ExplainPropertyText("Inner Unique", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 8a04294..41b167f 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -288,10 +288,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we to only match one
+					 * inner tuple, or if there's only one possible inner
+					 * matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI ||
+						node->js.inner_unique)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -402,6 +404,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.inner_unique = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 105e2dc..8f2ab14 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -802,10 +802,11 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we to only match one
+					 * inner tuple, or if there's only one possible inner
+					 * matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI || node->js.inner_unique)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -826,7 +827,27 @@ ExecMergeJoin(MergeJoinState *node)
 						InstrCountFiltered2(node, 1);
 				}
 				else
+				{
+					/*
+					 * If the inner side of the join is unique, then it's not
+					 * possible that we'll find another row matching this
+					 * outer row, so we can now move along to the next outer
+					 * row.
+					 * The correctness of this relies on none of the proof
+					 * quals used to prove the uniqueness being in the
+					 * joinqual. Since analyzejoins.c only uses quals with
+					 * mergeopfamilies, then this is safe. If that were to
+					 * ever change, then this would need to be revisitied.
+					 *
+					 * We're unable to perform the same optimization for
+					 * SEMI_JOINs, as there might be another row matching the
+					 * join condition.
+					 */
+					if (node->js.inner_unique)
+						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
+
 					InstrCountFiltered1(node, 1);
+				}
 				break;
 
 				/*
@@ -1048,7 +1069,11 @@ ExecMergeJoin(MergeJoinState *node)
 					/*
 					 * the merge clause matched so now we restore the inner
 					 * scan position to the first mark, and go join that tuple
-					 * (and any following ones) to the new outer.
+					 * (and any following ones) to the new outer. In the case
+					 * where we've only joined to the first inner tuple, we've
+					 * not advanced the inner side any away from where the
+					 * restore point would normally be, so have no need to
+					 * perform the restore.
 					 *
 					 * NOTE: we do not need to worry about the MatchedInner
 					 * state for the rescanned inner tuples.  We know all of
@@ -1062,17 +1087,21 @@ ExecMergeJoin(MergeJoinState *node)
 					 * forcing the merge clause to never match, so we never
 					 * get here.
 					 */
-					ExecRestrPos(innerPlan);
+					if (node->js.jointype != JOIN_SEMI &&
+						!node->js.inner_unique)
+					{
+						ExecRestrPos(innerPlan);
 
-					/*
-					 * ExecRestrPos probably should give us back a new Slot,
-					 * but since it doesn't, use the marked slot.  (The
-					 * previously returned mj_InnerTupleSlot cannot be assumed
-					 * to hold the required tuple.)
-					 */
-					node->mj_InnerTupleSlot = innerTupleSlot;
-					/* we need not do MJEvalInnerValues again */
+						/*
+						 * ExecRestrPos probably should give us back a new
+						 * Slot, but since it doesn't, use the marked slot.
+						 * (The previously returned mj_InnerTupleSlot cannot
+						 * be assumed to hold the required tuple.)
+						 */
+						node->mj_InnerTupleSlot = innerTupleSlot;
+					}
 
+					/* we need not do MJEvalInnerValues again */
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
@@ -1172,9 +1201,18 @@ ExecMergeJoin(MergeJoinState *node)
 
 				if (compareResult == 0)
 				{
-					ExecMarkPos(innerPlan);
+					/*
+					 * When we're only going join to a single inner tuple then
+					 * we'll not advance the inner side further on the current
+					 * outer tuple, so have no need to perform a Mark.
+					 */
+					if (node->js.jointype != JOIN_SEMI &&
+						!node->js.inner_unique)
+					{
+						ExecMarkPos(innerPlan);
 
-					MarkInnerTuple(node->mj_InnerTupleSlot, node);
+						MarkInnerTuple(node->mj_InnerTupleSlot, node);
+					}
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
@@ -1438,6 +1476,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.inner_unique = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index cac7ba1..dd1c4ed 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -219,10 +219,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we to only match one inner
+			 * tuple, or if there's only one possible inner matching tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.jointype == JOIN_SEMI || node->js.inner_unique)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -273,6 +273,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.inner_unique = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index a43daa7..52d8455 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1852,15 +1852,13 @@ cost_group(Path *path, PlannerInfo *root,
  * 'jointype' is the type of join to be performed
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -1891,10 +1889,13 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -1927,17 +1928,16 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  *
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->outerjoinpath;
 	Path	   *inner_path = path->innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	Cost		startup_cost = workspace->startup_cost;
@@ -1972,10 +1972,13 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2116,7 +2119,7 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
  * 'inner_path' is the inner input to the join
  * 'outersortkeys' is the list of sort keys for the outer path
  * 'innersortkeys' is the list of sort keys for the inner path
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  *
  * Note: outersortkeys and innersortkeys should be NIL if no explicit
  * sort is needed because the respective source path is already ordered.
@@ -2127,7 +2130,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo)
+					   JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2204,7 +2207,8 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerstartsel = cache->leftstartsel;
 			innerendsel = cache->leftendsel;
 		}
-		if (jointype == JOIN_LEFT ||
+		if (extra->inner_unique ||
+			jointype == JOIN_LEFT ||
 			jointype == JOIN_ANTI)
 		{
 			outerstartsel = 0.0;
@@ -2342,12 +2346,12 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		materialize_inner
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo)
+					 JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
@@ -2431,9 +2435,14 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * computations?
 	 *
 	 * The whole issue is moot if we are working from a unique-ified outer
-	 * input.
+	 * input. We'll also never rescan during SEMI/ANTI/inner_unique as there's
+	 * no need to advance the inner side to find subsequent matches, therefore
+	 * no need to mark/restore.
 	 */
-	if (IsA(outer_path, UniquePath))
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		IsA(outer_path, UniquePath))
 		rescannedtuples = 0;
 	else
 	{
@@ -2482,7 +2491,10 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	/*
 	 * Even if materializing doesn't look cheaper, we *must* do it if the
 	 * inner path is to be used directly (without sorting) and it doesn't
-	 * support mark/restore.
+	 * support mark/restore. However, if the inner side of the join is
+	 * guaranteed not to duplicate outer rows, as in the case with SEMI/ANTI
+	 * JOINs and joins which have an inner unique side, then we'll never need
+	 * to perform mark/restore.
 	 *
 	 * Since the inner side must be ordered, and only Sorts and IndexScans can
 	 * create order to begin with, and they both support mark/restore, you
@@ -2495,7 +2507,9 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * materialization is required for correctness in this case, and turning
 	 * it off does not entitle us to deliver an invalid plan.
 	 */
-	else if (innersortkeys == NIL &&
+	else if (innersortkeys == NIL && !extra->inner_unique &&
+			 path->jpath.jointype != JOIN_SEMI &&
+			 path->jpath.jointype != JOIN_ANTI &&
 			 !ExecSupportsMarkRestore(inner_path))
 		path->materialize_inner = true;
 
@@ -2637,16 +2651,14 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
  * 'hashclauses' is the list of joinclauses to be used as hash clauses
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2731,17 +2743,16 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	List	   *hashclauses = path->path_hashclauses;
@@ -2859,13 +2870,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 7c30ec6..cbf91f1 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -21,6 +21,7 @@
 #include "optimizer/cost.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/planmain.h"
 
 /* Hook for plugins to get control in add_paths_to_joinrel() */
 set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
@@ -103,6 +104,13 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.
+	 */
+	extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+											jointype, sjinfo, restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -318,8 +326,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * methodology worthwhile.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -330,11 +337,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -381,8 +386,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
 
@@ -392,11 +396,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -452,7 +454,7 @@ try_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -463,10 +465,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -516,8 +517,7 @@ try_hashjoin_path(PlannerInfo *root,
 	 * never have any output pathkeys, per comments in create_hashjoin_path.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -528,11 +528,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -579,8 +577,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, NIL))
 		return;
 
@@ -590,11 +587,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 438baf1..086d457 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -41,6 +41,10 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
 
 
 /*
@@ -847,3 +851,165 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/*
+		 * XXX Beware of making changes here. Merge Join's unique inner
+		 * optimization relies on all quals which make the join unique being
+		 * part of the initial join condition.
+		 */
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' is unique based on the join
+ *	  condition in 'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist)
+{
+	MemoryContext	old_context;
+	ListCell	   *lc;
+	int				innerrelid;
+	bool			unique = false;
+
+	/* can't prove uniqueness for joins with an empty restrictlist */
+	if (restrictlist == NIL)
+		return false;
+
+	/*
+	 * Currently we're unable to prove uniqueness when there's more than one
+	 * relation on the inner side of the join.
+	 */
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/*
+	 * First let's query the unique and non-unique caches to see if we've
+	 * managed to prove that innerrel is unique for some subset of this
+	 * outerrel. We don't need an exact match, as if we have any extra
+	 * outerrels than were previously cached, then they can't make the
+	 * innerrel any less unique.
+	 */
+	foreach(lc, root->unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(unique_rels, outerrel->relids))
+			return true;		/* Success! */
+	}
+
+	/*
+	 * We may have previously determined that this outerrel, or some superset
+	 * thereof, cannot prove this innerrel to be unique.
+	 */
+	foreach(lc, root->non_unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(outerrel->relids, unique_rels))
+			return false;
+	}
+
+	/* No cached information, so try to make the proof. */
+	if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+	{
+		unique = true;		/* Success! */
+
+		/*
+		 * Cache the positive result for future probes, being sure to keep it
+		 * in the planner_cxt even if we are working in GEQO.
+		 *
+		 * Note: one might consider trying to isolate the minimal subset of
+		 * the outerrels that proved the innerrel unique.  But it's not worth
+		 * the trouble, because the planner builds up joinrels incrementally
+		 * and so we'll see the minimally sufficient outerrels before any
+		 * supersets of them anyway.
+		 */
+		old_context = MemoryContextSwitchTo(root->planner_cxt);
+		root->unique_rels[innerrelid] =
+			lappend(root->unique_rels[innerrelid],
+					bms_copy(outerrel->relids));
+		MemoryContextSwitchTo(old_context);
+	}
+	else
+	{
+		/*
+		 * None of the join conditions for outerrel proved innerrel unique, so
+		 * we can safely reject this outerrel or any subset of it in future
+		 * checks.
+		 *
+		 * However, in normal planning mode, caching this knowledge is totally
+		 * pointless; it won't be queried again, because we build up joinrels
+		 * from smaller to larger.  It is useful in GEQO mode, where the
+		 * knowledge can be carried across successive planning attempts; and
+		 * it's likely to be useful when using join-search plugins, too.
+		 * Hence cache only when join_search_private is non-NULL.  (Yeah,
+		 * that's a hack, but it seems reasonable.)
+		 */
+		if (root->join_search_private)
+		{
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->non_unique_rels[innerrelid] =
+				lappend(root->non_unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+	}
+	return unique;
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index fae1f67..c60c2c9 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -206,12 +206,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -226,7 +226,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3532,7 +3532,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -3835,7 +3835,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -3975,7 +3975,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5112,7 +5112,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5121,9 +5121,10 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5135,7 +5136,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5145,8 +5146,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5187,7 +5189,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5201,8 +5203,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index e880759..4a36b82 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index f440875..ebb0e40 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1935,11 +1935,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -1950,16 +1948,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -1995,7 +1992,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2008,8 +2005,9 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2022,10 +2020,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2038,10 +2035,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2049,6 +2045,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2058,7 +2055,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2071,12 +2068,13 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2088,11 +2086,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2102,15 +2098,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2120,7 +2115,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2145,10 +2140,11 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index adc1db9..d1bda16 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -81,6 +81,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f9bcdd6..91e97d8 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1669,6 +1669,8 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		inner_unique;	/* True if each outer tuple can match to no
+								 * more than one inner tuple.*/
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index f72f7a8..f2ebee2 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -603,6 +603,8 @@ typedef struct CustomScan
  * jointype:	rule for joining tuples from left and right subtrees
  * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
  *				(plan.qual contains conditions that came from WHERE)
+ * inner_unique	each outer tuple can match to no more than one inner
+ *				tuple
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -618,6 +620,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 643be54..1b81579 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -222,6 +222,18 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner side based on the join condition.
+	 * This is a rather expensive test to perform, and requires looking for
+	 * proofs such as a relation's unique indexes.  We use this cache during
+	 * the join search to record lists of the sets of relations which both
+	 * prove, and disprove the uniqueness properties for the relid indexed by
+	 * these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1217,6 +1229,9 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* each outer tuple can match to no more than
+								 * one inner tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -2042,6 +2057,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique true if proofs exist which prove that outer side tuples match
+ *		no more than one inner side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2050,6 +2067,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 0e68264..6f22bcd 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -122,33 +122,29 @@ extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void initial_cost_mergejoin(PlannerInfo *root,
 					   JoinCostWorkspace *workspace,
 					   JoinType jointype,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo);
+					   JoinPathExtraData *extra);
 extern void final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo);
+					 JoinPathExtraData *extra);
 extern void initial_cost_hashjoin(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void cost_gather(GatherPath *path, PlannerInfo *root,
 			RelOptInfo *baserel, ParamPathInfo *param_info, double *rows);
 extern void cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 7b41317..5ce410c 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -102,11 +102,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -114,10 +112,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -128,11 +125,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					  JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -238,6 +233,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..e37c647 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -105,7 +105,11 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
-
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist);
 /*
  * prototypes for plan/setrefs.c
  */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..9784a87 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,7 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +391,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -982,29 +983,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 564218b..a96b2a1 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -317,12 +317,11 @@ explain (costs off)
                      ->  Index Scan using ec1_expr2 on ec1 ec1_1
                      ->  Index Scan using ec1_expr3 on ec1 ec1_2
                      ->  Index Scan using ec1_expr4 on ec1 ec1_3
-               ->  Materialize
-                     ->  Sort
-                           Sort Key: ec1.f1 USING <
-                           ->  Index Scan using ec1_pkey on ec1
-                                 Index Cond: (ff = '42'::bigint)
-(20 rows)
+               ->  Sort
+                     Sort Key: ec1.f1 USING <
+                     ->  Index Scan using ec1_pkey on ec1
+                           Index Cond: (ff = '42'::bigint)
+(19 rows)
 
 -- check partially indexed scan
 set enable_nestloop = on;
@@ -374,12 +373,11 @@ explain (costs off)
                Sort Key: (((ec1_2.ff + 3) + 1))
                ->  Seq Scan on ec1 ec1_2
          ->  Index Scan using ec1_expr4 on ec1 ec1_3
-   ->  Materialize
-         ->  Sort
-               Sort Key: ec1.f1 USING <
-               ->  Index Scan using ec1_pkey on ec1
-                     Index Cond: (ff = '42'::bigint)
-(14 rows)
+   ->  Sort
+         Sort Key: ec1.f1 USING <
+         ->  Index Scan using ec1_pkey on ec1
+               Index Cond: (ff = '42'::bigint)
+(13 rows)
 
 -- check effects of row-level security
 set enable_nestloop = on;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index d9bbae0..faf52dd 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3370,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3414,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3426,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3446,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3479,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3491,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3516,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3550,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3562,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3590,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3622,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3642,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3672,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3688,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3713,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3736,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3762,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3778,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3793,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3823,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3850,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3938,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3951,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -3979,7 +4012,7 @@ select id from a where id in (
 );
          QUERY PLAN         
 ----------------------------
- Hash Semi Join
+ Hash Join
    Hash Cond: (a.id = b.id)
    ->  Seq Scan on a
    ->  Hash
@@ -4801,12 +4834,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4833,12 +4867,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4866,6 +4901,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4873,7 +4909,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4893,12 +4929,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4920,10 +4957,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4932,7 +4971,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4978,16 +5017,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5054,13 +5095,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5076,7 +5121,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5094,17 +5139,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5126,7 +5176,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5139,16 +5189,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5161,10 +5213,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5173,7 +5227,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5200,10 +5254,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5225,7 +5281,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5323,3 +5379,283 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop Left Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 79513e4..d7d2376 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,13 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(8 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 56481de..ff9cc77 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2010,12 +2010,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2036,11 +2037,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 47afdc3..071daf5 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -814,6 +814,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -822,7 +823,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -842,6 +843,7 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -859,7 +861,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 3b7f689..3e4c3fb 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2195,6 +2195,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2202,6 +2203,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2209,6 +2211,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2216,12 +2219,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 97bccec..a39ae9f 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1730,3 +1730,106 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+
+drop table j1;
+drop table j2;
+drop table j3;
#113David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#110)
Re: Performance improvement for joins where outer side is unique

On 31 January 2017 at 10:43, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

I don't think that's possible. The whole point that the current join
removal code retries to remove joins which it already tried to remove,
after a successful removal is exactly because it is possible for a
join to become provability unique on the removal of another join.

Not seeing that ... example please?

I had a quick look at this again as it had been a while since I noticed that.

The sample case was:

create temp table uniquetbl (f1 text unique);
explain (costs off)
select t1.* from
uniquetbl as t1
left join (select *, '***'::text as d1 from uniquetbl) t2
on t1.f1 = t2.f1
left join uniquetbl t3
on t2.d1 = t3.f1;

However, what it actually fails on depends on if you check for unused
columns or uniqueness first as initially the subquery fails both of
the tests.

I was under the impression it was failing the unique test, as that's
what I was doing first in my patch.

If you test uniqueness first it'll fail on:

/*
* If such a clause actually references the inner rel then join
* removal has to be disallowed. We have to check this despite
* the previous attr_needed checks because of the possibility of
* pushed-down clauses referencing the rel.
*/
if (bms_is_member(innerrelid, restrictinfo->clause_relids))
return false;

but if you test for unused columns first, it'll fail on:

if (bms_is_subset(phinfo->ph_eval_at, innerrel->relids))
return false; /* there isn't any other place to eval PHV */

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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

#114Michael Paquier
michael.paquier@gmail.com
In reply to: David Rowley (#112)
Re: Performance improvement for joins where outer side is unique

On Tue, Jan 31, 2017 at 9:13 AM, David Rowley
<david.rowley@2ndquadrant.com> wrote:

On 31 January 2017 at 13:10, David Rowley <david.rowley@2ndquadrant.com> wrote:

I've attached a patch which implements this.

Please disregards previous patch. (I forgot git commit before git diff
to make the patch)

I've attached the correct patch.

Moved to CF 2017-03. (You are the last one.)
--
Michael

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

#115Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#111)
Re: Performance improvement for joins where outer side is unique

[ getting back to this patch finally... ]

David Rowley <david.rowley@2ndquadrant.com> writes:

I've attached a patch which implements this, though only for
MergeJoin, else I'd imagine we'd also need to ensure all proofs used
for testing the uniqueness were also hash-able too. I added some XXX
comments in analyzejoin.c around the mergeopfamilies == NIL tests to
mention that Merge Join depends on all the unique proof quals having
mergeopfamilies. This also assumes we'll never use some subset of
mergejoin-able quals for a merge join, which could be an interesting
area in the future, as we might have some btree index on a subset of
those columns to provide pre-sorted input. In short, it's a great
optimisation, but I'm a little scared we might break it one day.

Umm ... it's broken already isn't it? See the comment for
generate_mergejoin_paths:

* 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.)

There's another, more subtle, way in which it could fail in
sort_inner_and_outer():

* 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
* some sort orders may be more useful than others for higher-level
* mergejoins, so it's worth considering multiple orderings.
*
* Actually, it's not quite true that every mergeclause ordering will
* generate a different path order, because some of the clauses may be
* partially redundant (refer to the same EquivalenceClasses). Therefore,
* what we do is convert the mergeclause list to a list of canonical
* pathkeys, and then consider different orderings of the pathkeys.

I'm fairly sure that it's possible to end up with fewer pathkeys than
there are mergeclauses in this code path. Now, you might be all right
anyway given that the mergeclauses must refer to the same ECs in such a
case --- maybe they're fully redundant and we can take testing the
included clause as proving the omitted one(s) too. I'm not certain
right now what I meant by "partially redundant" in this comment.
But in any case, it's moot for the present purpose because
generate_mergejoin_paths certainly breaks your assumption.

regards, tom lane

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

#116David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#115)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 14 March 2017 at 07:50, Tom Lane <tgl@sss.pgh.pa.us> wrote:

[ getting back to this patch finally... ]

David Rowley <david.rowley@2ndquadrant.com> writes:

I've attached a patch which implements this, though only for
MergeJoin, else I'd imagine we'd also need to ensure all proofs used
for testing the uniqueness were also hash-able too. I added some XXX
comments in analyzejoin.c around the mergeopfamilies == NIL tests to
mention that Merge Join depends on all the unique proof quals having
mergeopfamilies. This also assumes we'll never use some subset of
mergejoin-able quals for a merge join, which could be an interesting
area in the future, as we might have some btree index on a subset of
those columns to provide pre-sorted input. In short, it's a great
optimisation, but I'm a little scared we might break it one day.

Umm ... it's broken already isn't it? See the comment for
generate_mergejoin_paths:

* 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.)

There's another, more subtle, way in which it could fail in
sort_inner_and_outer():

* 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
* some sort orders may be more useful than others for higher-level
* mergejoins, so it's worth considering multiple orderings.
*
* Actually, it's not quite true that every mergeclause ordering will
* generate a different path order, because some of the clauses may be
* partially redundant (refer to the same EquivalenceClasses). Therefore,
* what we do is convert the mergeclause list to a list of canonical
* pathkeys, and then consider different orderings of the pathkeys.

I'm fairly sure that it's possible to end up with fewer pathkeys than
there are mergeclauses in this code path. Now, you might be all right
anyway given that the mergeclauses must refer to the same ECs in such a
case --- maybe they're fully redundant and we can take testing the
included clause as proving the omitted one(s) too. I'm not certain
right now what I meant by "partially redundant" in this comment.
But in any case, it's moot for the present purpose because
generate_mergejoin_paths certainly breaks your assumption.

Thanks for looking at this again.

Yeah confirmed. It's broken. I guess I just need to remember in the Path if
we got all the join quals, although I've not looked in detail what the best
fix is. I imagine it'll require storing something else in the JoinPath.

Here's my test case:

select 'create tablespace slow_disk LOCATION ''' ||
current_setting('data_directory') || ''' WITH (random_page_cost=1000);';
\gexec
create table ab (a int not null, b int not null);
create unique index ab_pkey on ab (a,b) tablespace slow_disk;
alter table ab add constraint ab_pkey primary key using index ab_pkey;

create index ab_a_idx on ab (a);

insert into ab values(1,1);
insert into ab values(1,2);

set enable_nestloop to 0;
set enable_hashjoin to 0;
set enable_sort to 0;

explain verbose select * from ab ab1 inner join ab ab2 on ab1.a = ab2.a and
ab1.b = ab2.b;
QUERY PLAN

-------------------------------------------------------------------------------------
Merge Join (cost=0.26..24.35 rows=1 width=16)
Output: ab1.a, ab1.b, ab2.a, ab2.b
Inner Unique: Yes
Merge Cond: (ab1.a = ab2.a)
Join Filter: (ab1.b = ab2.b)
-> Index Scan using ab_a_idx on public.ab ab1 (cost=0.13..12.16 rows=2
width=8)
Output: ab1.a, ab1.b
-> Index Scan using ab_a_idx on public.ab ab2 (cost=0.13..12.16 rows=2
width=8)
Output: ab2.a, ab2.b
(9 rows)

select * from ab ab1 inner join ab ab2 on ab1.a = ab2.a and ab1.b = ab2.b;
-- wrong results
a | b | a | b
---+---+---+---
1 | 2 | 1 | 2
(1 row)

drop index ab_a_idx;

-- same query again. This time it'll use the PK index on the slow_disk
tablespace
select * from ab ab1 inner join ab ab2 on ab1.a = ab2.a and ab1.b = ab2.b;
a | b | a | b
---+---+---+---
1 | 1 | 1 | 1
1 | 2 | 1 | 2
(2 rows)

I had to fix up some conflicts before testing this again on a recent
master, so I've attached my rebased version. No fix yet for the above issue

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-03-14.patchapplication/octet-stream; name=unique_joins_2017-03-14.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c9b55ea..c91f752 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1329,6 +1329,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+				ExplainPropertyText("Inner Unique", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index c50d93f..dd35604 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -288,10 +288,12 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we to only match one
+					 * inner tuple, or if there's only one possible inner
+					 * matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI ||
+						node->js.inner_unique)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -402,6 +404,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.inner_unique = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 105e2dc..8f2ab14 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -802,10 +802,11 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we to only match one
+					 * inner tuple, or if there's only one possible inner
+					 * matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.jointype == JOIN_SEMI || node->js.inner_unique)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -826,7 +827,27 @@ ExecMergeJoin(MergeJoinState *node)
 						InstrCountFiltered2(node, 1);
 				}
 				else
+				{
+					/*
+					 * If the inner side of the join is unique, then it's not
+					 * possible that we'll find another row matching this
+					 * outer row, so we can now move along to the next outer
+					 * row.
+					 * The correctness of this relies on none of the proof
+					 * quals used to prove the uniqueness being in the
+					 * joinqual. Since analyzejoins.c only uses quals with
+					 * mergeopfamilies, then this is safe. If that were to
+					 * ever change, then this would need to be revisitied.
+					 *
+					 * We're unable to perform the same optimization for
+					 * SEMI_JOINs, as there might be another row matching the
+					 * join condition.
+					 */
+					if (node->js.inner_unique)
+						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
+
 					InstrCountFiltered1(node, 1);
+				}
 				break;
 
 				/*
@@ -1048,7 +1069,11 @@ ExecMergeJoin(MergeJoinState *node)
 					/*
 					 * the merge clause matched so now we restore the inner
 					 * scan position to the first mark, and go join that tuple
-					 * (and any following ones) to the new outer.
+					 * (and any following ones) to the new outer. In the case
+					 * where we've only joined to the first inner tuple, we've
+					 * not advanced the inner side any away from where the
+					 * restore point would normally be, so have no need to
+					 * perform the restore.
 					 *
 					 * NOTE: we do not need to worry about the MatchedInner
 					 * state for the rescanned inner tuples.  We know all of
@@ -1062,17 +1087,21 @@ ExecMergeJoin(MergeJoinState *node)
 					 * forcing the merge clause to never match, so we never
 					 * get here.
 					 */
-					ExecRestrPos(innerPlan);
+					if (node->js.jointype != JOIN_SEMI &&
+						!node->js.inner_unique)
+					{
+						ExecRestrPos(innerPlan);
 
-					/*
-					 * ExecRestrPos probably should give us back a new Slot,
-					 * but since it doesn't, use the marked slot.  (The
-					 * previously returned mj_InnerTupleSlot cannot be assumed
-					 * to hold the required tuple.)
-					 */
-					node->mj_InnerTupleSlot = innerTupleSlot;
-					/* we need not do MJEvalInnerValues again */
+						/*
+						 * ExecRestrPos probably should give us back a new
+						 * Slot, but since it doesn't, use the marked slot.
+						 * (The previously returned mj_InnerTupleSlot cannot
+						 * be assumed to hold the required tuple.)
+						 */
+						node->mj_InnerTupleSlot = innerTupleSlot;
+					}
 
+					/* we need not do MJEvalInnerValues again */
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
@@ -1172,9 +1201,18 @@ ExecMergeJoin(MergeJoinState *node)
 
 				if (compareResult == 0)
 				{
-					ExecMarkPos(innerPlan);
+					/*
+					 * When we're only going join to a single inner tuple then
+					 * we'll not advance the inner side further on the current
+					 * outer tuple, so have no need to perform a Mark.
+					 */
+					if (node->js.jointype != JOIN_SEMI &&
+						!node->js.inner_unique)
+					{
+						ExecMarkPos(innerPlan);
 
-					MarkInnerTuple(node->mj_InnerTupleSlot, node);
+						MarkInnerTuple(node->mj_InnerTupleSlot, node);
+					}
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
@@ -1438,6 +1476,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.inner_unique = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index cac7ba1..dd1c4ed 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -219,10 +219,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we to only match one inner
+			 * tuple, or if there's only one possible inner matching tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.jointype == JOIN_SEMI || node->js.inner_unique)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -273,6 +273,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.inner_unique = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index e78f3a8..849e295 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2032,15 +2032,13 @@ cost_group(Path *path, PlannerInfo *root,
  * 'jointype' is the type of join to be performed
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2071,10 +2069,13 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -2107,17 +2108,16 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  *
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->outerjoinpath;
 	Path	   *inner_path = path->innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	Cost		startup_cost = workspace->startup_cost;
@@ -2152,10 +2152,13 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2296,7 +2299,7 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
  * 'inner_path' is the inner input to the join
  * 'outersortkeys' is the list of sort keys for the outer path
  * 'innersortkeys' is the list of sort keys for the inner path
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  *
  * Note: outersortkeys and innersortkeys should be NIL if no explicit
  * sort is needed because the respective source path is already ordered.
@@ -2307,7 +2310,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo)
+					   JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2384,7 +2387,8 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerstartsel = cache->leftstartsel;
 			innerendsel = cache->leftendsel;
 		}
-		if (jointype == JOIN_LEFT ||
+		if (extra->inner_unique ||
+			jointype == JOIN_LEFT ||
 			jointype == JOIN_ANTI)
 		{
 			outerstartsel = 0.0;
@@ -2522,12 +2526,12 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		materialize_inner
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo)
+					 JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
@@ -2611,9 +2615,14 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * computations?
 	 *
 	 * The whole issue is moot if we are working from a unique-ified outer
-	 * input.
+	 * input. We'll also never rescan during SEMI/ANTI/inner_unique as there's
+	 * no need to advance the inner side to find subsequent matches, therefore
+	 * no need to mark/restore.
 	 */
-	if (IsA(outer_path, UniquePath))
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		IsA(outer_path, UniquePath))
 		rescannedtuples = 0;
 	else
 	{
@@ -2662,7 +2671,10 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	/*
 	 * Even if materializing doesn't look cheaper, we *must* do it if the
 	 * inner path is to be used directly (without sorting) and it doesn't
-	 * support mark/restore.
+	 * support mark/restore. However, if the inner side of the join is
+	 * guaranteed not to duplicate outer rows, as in the case with SEMI/ANTI
+	 * JOINs and joins which have an inner unique side, then we'll never need
+	 * to perform mark/restore.
 	 *
 	 * Since the inner side must be ordered, and only Sorts and IndexScans can
 	 * create order to begin with, and they both support mark/restore, you
@@ -2675,7 +2687,9 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * materialization is required for correctness in this case, and turning
 	 * it off does not entitle us to deliver an invalid plan.
 	 */
-	else if (innersortkeys == NIL &&
+	else if (innersortkeys == NIL && !extra->inner_unique &&
+			 path->jpath.jointype != JOIN_SEMI &&
+			 path->jpath.jointype != JOIN_ANTI &&
 			 !ExecSupportsMarkRestore(inner_path))
 		path->materialize_inner = true;
 
@@ -2817,16 +2831,14 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
  * 'hashclauses' is the list of joinclauses to be used as hash clauses
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2911,17 +2923,16 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	List	   *hashclauses = path->path_hashclauses;
@@ -3037,13 +3048,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index de7044d..ae89e89 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -21,6 +21,7 @@
 #include "optimizer/cost.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/planmain.h"
 
 /* Hook for plugins to get control in add_paths_to_joinrel() */
 set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
@@ -121,6 +122,13 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.
+	 */
+	extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+											jointype, sjinfo, restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -336,8 +344,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * methodology worthwhile.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -348,11 +355,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -399,8 +404,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
 
@@ -410,11 +414,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -486,7 +488,7 @@ try_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -497,10 +499,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -562,7 +563,7 @@ try_partial_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
@@ -573,10 +574,9 @@ try_partial_mergejoin_path(PlannerInfo *root,
 										   joinrel,
 										   jointype,
 										   &workspace,
-										   extra->sjinfo,
+										   extra,
 										   outer_path,
 										   inner_path,
-										   extra->restrictlist,
 										   pathkeys,
 										   NULL,
 										   mergeclauses,
@@ -620,8 +620,7 @@ try_hashjoin_path(PlannerInfo *root,
 	 * never have any output pathkeys, per comments in create_hashjoin_path.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -632,11 +631,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -683,8 +680,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, NIL))
 		return;
 
@@ -694,11 +690,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index ac63f75..b48167e 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -41,6 +41,10 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
 
 
 /*
@@ -845,3 +849,165 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/*
+		 * XXX Beware of making changes here. Merge Join's unique inner
+		 * optimization relies on all quals which make the join unique being
+		 * part of the initial join condition.
+		 */
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' is unique based on the join
+ *	  condition in 'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist)
+{
+	MemoryContext	old_context;
+	ListCell	   *lc;
+	int				innerrelid;
+	bool			unique = false;
+
+	/* can't prove uniqueness for joins with an empty restrictlist */
+	if (restrictlist == NIL)
+		return false;
+
+	/*
+	 * Currently we're unable to prove uniqueness when there's more than one
+	 * relation on the inner side of the join.
+	 */
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/*
+	 * First let's query the unique and non-unique caches to see if we've
+	 * managed to prove that innerrel is unique for some subset of this
+	 * outerrel. We don't need an exact match, as if we have any extra
+	 * outerrels than were previously cached, then they can't make the
+	 * innerrel any less unique.
+	 */
+	foreach(lc, root->unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(unique_rels, outerrel->relids))
+			return true;		/* Success! */
+	}
+
+	/*
+	 * We may have previously determined that this outerrel, or some superset
+	 * thereof, cannot prove this innerrel to be unique.
+	 */
+	foreach(lc, root->non_unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(outerrel->relids, unique_rels))
+			return false;
+	}
+
+	/* No cached information, so try to make the proof. */
+	if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+	{
+		unique = true;		/* Success! */
+
+		/*
+		 * Cache the positive result for future probes, being sure to keep it
+		 * in the planner_cxt even if we are working in GEQO.
+		 *
+		 * Note: one might consider trying to isolate the minimal subset of
+		 * the outerrels that proved the innerrel unique.  But it's not worth
+		 * the trouble, because the planner builds up joinrels incrementally
+		 * and so we'll see the minimally sufficient outerrels before any
+		 * supersets of them anyway.
+		 */
+		old_context = MemoryContextSwitchTo(root->planner_cxt);
+		root->unique_rels[innerrelid] =
+			lappend(root->unique_rels[innerrelid],
+					bms_copy(outerrel->relids));
+		MemoryContextSwitchTo(old_context);
+	}
+	else
+	{
+		/*
+		 * None of the join conditions for outerrel proved innerrel unique, so
+		 * we can safely reject this outerrel or any subset of it in future
+		 * checks.
+		 *
+		 * However, in normal planning mode, caching this knowledge is totally
+		 * pointless; it won't be queried again, because we build up joinrels
+		 * from smaller to larger.  It is useful in GEQO mode, where the
+		 * knowledge can be carried across successive planning attempts; and
+		 * it's likely to be useful when using join-search plugins, too.
+		 * Hence cache only when join_search_private is non-NULL.  (Yeah,
+		 * that's a hack, but it seems reasonable.)
+		 */
+		if (root->join_search_private)
+		{
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->non_unique_rels[innerrelid] =
+				lappend(root->non_unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+	}
+	return unique;
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index d002e6d..fd9993e 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -211,12 +211,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -231,7 +231,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3676,7 +3676,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -3978,7 +3978,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -4118,7 +4118,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5290,7 +5290,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5299,9 +5299,10 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5313,7 +5314,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5323,8 +5324,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5365,7 +5367,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5379,8 +5381,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 3c58d05..415acfc 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 8ce772d..c82ff39 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -2019,11 +2019,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -2034,16 +2032,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -2079,7 +2076,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2092,8 +2089,9 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2106,10 +2104,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2122,10 +2119,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2133,6 +2129,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2142,7 +2139,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2155,12 +2152,13 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2172,11 +2170,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2186,15 +2182,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2204,7 +2199,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2229,10 +2224,11 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index caf8291..65c92bf 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -81,6 +81,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f856f60..85f931c 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1760,6 +1760,8 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		inner_unique;	/* True if each outer tuple can match to no
+								 * more than one inner tuple.*/
 } JoinState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index b880dc1..28638a2 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -615,6 +615,8 @@ typedef struct CustomScan
  * jointype:	rule for joining tuples from left and right subtrees
  * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
  *				(plan.qual contains conditions that came from WHERE)
+ * inner_unique	each outer tuple can match to no more than one inner
+ *				tuple
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -630,6 +632,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 05d6f07..59ceee1 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -222,6 +222,18 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner side based on the join condition.
+	 * This is a rather expensive test to perform, and requires looking for
+	 * proofs such as a relation's unique indexes.  We use this cache during
+	 * the join search to record lists of the sets of relations which both
+	 * prove, and disprove the uniqueness properties for the relid indexed by
+	 * these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1231,6 +1243,9 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* each outer tuple can match to no more than
+								 * one inner tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -2056,6 +2071,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique true if proofs exist which prove that outer side tuples match
+ *		no more than one inner side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2064,6 +2081,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index d9a9b12..ca33494 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -127,33 +127,29 @@ extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void initial_cost_mergejoin(PlannerInfo *root,
 					   JoinCostWorkspace *workspace,
 					   JoinType jointype,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo);
+					   JoinPathExtraData *extra);
 extern void final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo);
+					 JoinPathExtraData *extra);
 extern void initial_cost_hashjoin(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void cost_gather(GatherPath *path, PlannerInfo *root,
 			RelOptInfo *baserel, ParamPathInfo *param_info, double *rows);
 extern void cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 373c722..c5c4ac0 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -115,11 +115,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -127,10 +125,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -141,11 +138,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					  JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -251,6 +246,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..e37c647 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -105,7 +105,11 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
-
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist);
 /*
  * prototypes for plan/setrefs.c
  */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..9784a87 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,7 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +391,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -982,29 +983,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 564218b..a96b2a1 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -317,12 +317,11 @@ explain (costs off)
                      ->  Index Scan using ec1_expr2 on ec1 ec1_1
                      ->  Index Scan using ec1_expr3 on ec1 ec1_2
                      ->  Index Scan using ec1_expr4 on ec1 ec1_3
-               ->  Materialize
-                     ->  Sort
-                           Sort Key: ec1.f1 USING <
-                           ->  Index Scan using ec1_pkey on ec1
-                                 Index Cond: (ff = '42'::bigint)
-(20 rows)
+               ->  Sort
+                     Sort Key: ec1.f1 USING <
+                     ->  Index Scan using ec1_pkey on ec1
+                           Index Cond: (ff = '42'::bigint)
+(19 rows)
 
 -- check partially indexed scan
 set enable_nestloop = on;
@@ -374,12 +373,11 @@ explain (costs off)
                Sort Key: (((ec1_2.ff + 3) + 1))
                ->  Seq Scan on ec1 ec1_2
          ->  Index Scan using ec1_expr4 on ec1 ec1_3
-   ->  Materialize
-         ->  Sort
-               Sort Key: ec1.f1 USING <
-               ->  Index Scan using ec1_pkey on ec1
-                     Index Cond: (ff = '42'::bigint)
-(14 rows)
+   ->  Sort
+         Sort Key: ec1.f1 USING <
+         ->  Index Scan using ec1_pkey on ec1
+               Index Cond: (ff = '42'::bigint)
+(13 rows)
 
 -- check effects of row-level security
 set enable_nestloop = on;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 4992048..bc3c597 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3370,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3414,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3426,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3446,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3479,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3491,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3516,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3550,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3562,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3590,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3622,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3642,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3672,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3688,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3713,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3736,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3762,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3778,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3793,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3823,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3850,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3938,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3951,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -3979,7 +4012,7 @@ select id from a where id in (
 );
          QUERY PLAN         
 ----------------------------
- Hash Semi Join
+ Hash Join
    Hash Cond: (a.id = b.id)
    ->  Seq Scan on a
    ->  Hash
@@ -4805,12 +4838,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4837,12 +4871,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4870,6 +4905,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4877,7 +4913,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4897,12 +4933,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4924,10 +4961,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4936,7 +4975,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4982,16 +5021,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5058,13 +5099,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5080,7 +5125,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5098,17 +5143,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5130,7 +5180,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5143,16 +5193,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5165,10 +5217,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5177,7 +5231,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5204,10 +5258,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5229,7 +5285,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5327,3 +5383,283 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop Left Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 04848c1..e40bdf3 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,13 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(8 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 526a4ae..8df33be 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -1994,12 +1994,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2020,11 +2021,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index ed7d6d8..063cb13 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -837,6 +837,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -845,7 +846,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -865,6 +866,7 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -882,7 +884,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 3b7f689..3e4c3fb 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2195,6 +2195,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2202,6 +2203,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2209,6 +2211,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2216,12 +2219,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out
index d3bd8c9..7e0f3cf 100644
--- a/src/test/regress/expected/xml_1.out
+++ b/src/test/regress/expected/xml_1.out
@@ -926,12 +926,13 @@ EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1;
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- XMLNAMESPACES tests
 SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz),
@@ -1068,12 +1069,13 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- test qual
 SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) WHERE "COUNTRY_NAME" = 'Japan';
@@ -1087,13 +1089,14 @@ SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan"
 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
          Table Function Call: XMLTABLE(('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]'::text) PASSING (xmldata.data) COLUMNS "COUNTRY_NAME" text, "REGION_ID" integer)
          Filter: ("xmltable"."COUNTRY_NAME" = 'Japan'::text)
-(8 rows)
+(9 rows)
 
 -- should to work with more data
 INSERT INTO xmldata VALUES('<ROWS>
@@ -1186,13 +1189,14 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
          Filter: ("xmltable".region_id = 2)
-(8 rows)
+(9 rows)
 
 -- should fail, NULL value
 SELECT  xmltable.*
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index cca1a53..1692720 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1732,3 +1732,106 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(2,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+
+drop table j1;
+drop table j2;
+drop table j3;
#117David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#116)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 14 March 2017 at 11:35, David Rowley <david.rowley@2ndquadrant.com>
wrote:

On 14 March 2017 at 07:50, Tom Lane <tgl@sss.pgh.pa.us> wrote:

[ getting back to this patch finally... ]

David Rowley <david.rowley@2ndquadrant.com> writes:

I've attached a patch which implements this, though only for
MergeJoin, else I'd imagine we'd also need to ensure all proofs used
for testing the uniqueness were also hash-able too. I added some XXX
comments in analyzejoin.c around the mergeopfamilies == NIL tests to
mention that Merge Join depends on all the unique proof quals having
mergeopfamilies. This also assumes we'll never use some subset of
mergejoin-able quals for a merge join, which could be an interesting
area in the future, as we might have some btree index on a subset of
those columns to provide pre-sorted input. In short, it's a great
optimisation, but I'm a little scared we might break it one day.

Umm ... it's broken already isn't it? See the comment for
generate_mergejoin_paths:

Thanks for looking at this again.

Yeah confirmed. It's broken. I guess I just need to remember in the Path
if we got all the join quals, although I've not looked in detail what the
best fix is. I imagine it'll require storing something else in the JoinPath.

OK, so I've spent some more time on this and I've come up with a solution.

Basically the solution is to not skip mark and restore when joinquals
contains any items. This is a requirement for SEMI joins, but overly
cautious for unique joins. However, I think it'll apply in most cases when
Merge Join will be a win, and that's when there's a btree index on both
sides of the join which covers all columns in the join condition. I
carefully commented this part of the code to explain what can be done to
have it apply in more cases.

This caused me to go and change the following code too:

@@ -2676,6 +2688,9 @@ final_cost_mergejoin(PlannerInfo *root, MergePath
*path,
  * it off does not entitle us to deliver an invalid plan.
  */
  else if (innersortkeys == NIL &&
+ !((extra->inner_unique || path->jpath.jointype == JOIN_SEMI) &&
+ list_length(path->jpath.joinrestrictinfo) ==
+ list_length(path->path_mergeclauses)) &&
  !ExecSupportsMarkRestore(inner_path))
  path->materialize_inner = true;

I've been staring at this for a while and I think it's correct. If it's
wrong then we'd get "ERROR: unrecognized node type: <n>" from ExecMarkPos().

Here we must make sure and never skip materializing the inner side, if
we're not going to skip mark and restore in ExecInitMergeJoin().

I've attached a patch which fixes the issue.

Also added a regression test to try to make sure it stays fixed. There's a
few other mostly cosmetic fixes in there too.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-03-14a.patchapplication/octet-stream; name=unique_joins_2017-03-14a.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c9b55ea..c91f752 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1329,6 +1329,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+				ExplainPropertyText("Inner Unique", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index c50d93f..72f99b7 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -288,10 +288,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NIL ||
@@ -402,6 +402,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -447,8 +455,13 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			hjstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 105e2dc..12525f7 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -802,10 +802,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NIL ||
@@ -826,7 +826,18 @@ ExecMergeJoin(MergeJoinState *node)
 						InstrCountFiltered2(node, 1);
 				}
 				else
+				{
+					/*
+					 * SkipMarkRestore will be set if we only require a single
+					 * match on the inner side, or if there can only be one
+					 * match. We can safely skip to the next outer tuple in
+					 * this case.
+					 */
+					if (node->mj_SkipMarkRestore)
+						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
+
 					InstrCountFiltered1(node, 1);
+				}
 				break;
 
 				/*
@@ -1048,7 +1059,9 @@ ExecMergeJoin(MergeJoinState *node)
 					/*
 					 * the merge clause matched so now we restore the inner
 					 * scan position to the first mark, and go join that tuple
-					 * (and any following ones) to the new outer.
+					 * (and any following ones) to the new outer.  If we were
+					 * able to determine mark and restore would be a no-op,
+					 * then we can skip this rigmarole.
 					 *
 					 * NOTE: we do not need to worry about the MatchedInner
 					 * state for the rescanned inner tuples.  We know all of
@@ -1062,17 +1075,20 @@ ExecMergeJoin(MergeJoinState *node)
 					 * forcing the merge clause to never match, so we never
 					 * get here.
 					 */
-					ExecRestrPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecRestrPos(innerPlan);
 
-					/*
-					 * ExecRestrPos probably should give us back a new Slot,
-					 * but since it doesn't, use the marked slot.  (The
-					 * previously returned mj_InnerTupleSlot cannot be assumed
-					 * to hold the required tuple.)
-					 */
-					node->mj_InnerTupleSlot = innerTupleSlot;
-					/* we need not do MJEvalInnerValues again */
+						/*
+						 * ExecRestrPos probably should give us back a new
+						 * Slot, but since it doesn't, use the marked slot.
+						 * (The previously returned mj_InnerTupleSlot cannot
+						 * be assumed to hold the required tuple.)
+						 */
+						node->mj_InnerTupleSlot = innerTupleSlot;
+					}
 
+					/* we need not do MJEvalInnerValues again */
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
@@ -1172,9 +1188,12 @@ ExecMergeJoin(MergeJoinState *node)
 
 				if (compareResult == 0)
 				{
-					ExecMarkPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecMarkPos(innerPlan);
 
-					MarkInnerTuple(node->mj_InnerTupleSlot, node);
+						MarkInnerTuple(node->mj_InnerTupleSlot, node);
+					}
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
@@ -1438,6 +1457,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1504,8 +1531,13 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			mergestate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
@@ -1586,6 +1618,36 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->mj_InnerTupleSlot = NULL;
 
 	/*
+	 * When matching to the first inner tuple only, as in the case with unique
+	 * and SEMI joins, if the joinqual list is empty (i.e all join conditions
+	 * are present in as merge join clauses), then when we find an inner tuple
+	 * matching the outer tuple, we've no need to keep scanning the inner side
+	 * looking for more tuples matching this outer tuple.  This means we're
+	 * never going to advance the inner side to check for more tuples which
+	 * match the current outer side tuple.  This basically boil down to, any
+	 * mark/restore operations we would perform would be a no-op.  The inner
+	 * side will already be in the correct location.  Here we detect this and
+	 * record that no mark and restore is required during the join.
+	 *
+	 * This mark and restore skipping can provide a good performance boost,
+	 * and it is a fairly common case, so it's certainly worth the cost of the
+	 * additional checks which must be performed during the join.
+	 *
+	 * We could apply this optimization in more casee for unique joins. We'd
+	 * just need to ensure that all of the quals which were used to prove the
+	 * inner side is unique are present in the merge join clauses. Currently
+	 * we've no way to get this information from the planner, but it may be
+	 * worth some exploration for ways to improve this in the future. Here we
+	 * can assume all the unique quals are present as merge join quals by the
+	 * lack of any joinquals.  Further tuning in this area is not possible for
+	 * SEMI joins.
+	 *
+	 * XXX if changing this see logic in final_cost_mergejoin()
+	 */
+	mergestate->mj_SkipMarkRestore = mergestate->js.joinqual == NIL &&
+									 mergestate->js.first_inner_tuple_only;
+
+	/*
 	 * initialization successful
 	 */
 	MJ1_printf("ExecInitMergeJoin: %s\n",
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index cac7ba1..5b59c72 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -219,10 +219,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need to join to the
+			 * first inner matching tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.first_inner_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NIL || ExecQual(otherqual, econtext, false))
@@ -273,6 +273,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -316,8 +324,13 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			nlstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index e78f3a8..58d008b 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2032,15 +2032,13 @@ cost_group(Path *path, PlannerInfo *root,
  * 'jointype' is the type of join to be performed
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2071,10 +2069,13 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -2107,17 +2108,16 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  *
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->outerjoinpath;
 	Path	   *inner_path = path->innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	Cost		startup_cost = workspace->startup_cost;
@@ -2152,10 +2152,13 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2296,7 +2299,7 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
  * 'inner_path' is the inner input to the join
  * 'outersortkeys' is the list of sort keys for the outer path
  * 'innersortkeys' is the list of sort keys for the inner path
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  *
  * Note: outersortkeys and innersortkeys should be NIL if no explicit
  * sort is needed because the respective source path is already ordered.
@@ -2307,7 +2310,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo)
+					   JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2384,7 +2387,8 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerstartsel = cache->leftstartsel;
 			innerendsel = cache->leftendsel;
 		}
-		if (jointype == JOIN_LEFT ||
+		if (extra->inner_unique ||
+			jointype == JOIN_LEFT ||
 			jointype == JOIN_ANTI)
 		{
 			outerstartsel = 0.0;
@@ -2522,12 +2526,12 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		materialize_inner
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo)
+					 JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
@@ -2611,9 +2615,14 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * computations?
 	 *
 	 * The whole issue is moot if we are working from a unique-ified outer
-	 * input.
+	 * input. We'll also never rescan during SEMI/ANTI/inner_unique as there's
+	 * no need to advance the inner side to find subsequent matches, therefore
+	 * no need to mark/restore.
 	 */
-	if (IsA(outer_path, UniquePath))
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		IsA(outer_path, UniquePath))
 		rescannedtuples = 0;
 	else
 	{
@@ -2662,7 +2671,11 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	/*
 	 * Even if materializing doesn't look cheaper, we *must* do it if the
 	 * inner path is to be used directly (without sorting) and it doesn't
-	 * support mark/restore.
+	 * support mark/restore. However, if the inner side of the join is unique
+	 * or we're performing a SEMI join, and all of the joinrestrictinfos are
+	 * present as merge clauses, then we may be able to skip mark and restore
+	 * completely.  The logic here must follow the logic that is used to set
+	 * mj_SkipMarkRestore in ExecInitMergeJoin().
 	 *
 	 * Since the inner side must be ordered, and only Sorts and IndexScans can
 	 * create order to begin with, and they both support mark/restore, you
@@ -2676,6 +2689,9 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * it off does not entitle us to deliver an invalid plan.
 	 */
 	else if (innersortkeys == NIL &&
+			 !((extra->inner_unique || path->jpath.jointype == JOIN_SEMI) &&
+			 list_length(path->jpath.joinrestrictinfo) ==
+			 list_length(path->path_mergeclauses)) &&
 			 !ExecSupportsMarkRestore(inner_path))
 		path->materialize_inner = true;
 
@@ -2817,16 +2833,14 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
  * 'hashclauses' is the list of joinclauses to be used as hash clauses
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2911,17 +2925,16 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	List	   *hashclauses = path->path_hashclauses;
@@ -3037,13 +3050,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index de7044d..ae89e89 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -21,6 +21,7 @@
 #include "optimizer/cost.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/planmain.h"
 
 /* Hook for plugins to get control in add_paths_to_joinrel() */
 set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
@@ -121,6 +122,13 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.
+	 */
+	extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+											jointype, sjinfo, restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -336,8 +344,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * methodology worthwhile.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -348,11 +355,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -399,8 +404,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
 
@@ -410,11 +414,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -486,7 +488,7 @@ try_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -497,10 +499,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -562,7 +563,7 @@ try_partial_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
@@ -573,10 +574,9 @@ try_partial_mergejoin_path(PlannerInfo *root,
 										   joinrel,
 										   jointype,
 										   &workspace,
-										   extra->sjinfo,
+										   extra,
 										   outer_path,
 										   inner_path,
-										   extra->restrictlist,
 										   pathkeys,
 										   NULL,
 										   mergeclauses,
@@ -620,8 +620,7 @@ try_hashjoin_path(PlannerInfo *root,
 	 * never have any output pathkeys, per comments in create_hashjoin_path.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -632,11 +631,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -683,8 +680,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, NIL))
 		return;
 
@@ -694,11 +690,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index ac63f75..361cec9 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -41,6 +41,10 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
 
 
 /*
@@ -845,3 +849,159 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' is unique based on the join
+ *	  condition in 'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist)
+{
+	MemoryContext	old_context;
+	ListCell	   *lc;
+	int				innerrelid;
+
+	/* can't prove uniqueness for joins with an empty restrictlist */
+	if (restrictlist == NIL)
+		return false;
+
+	/*
+	 * Currently we're unable to prove uniqueness when there's more than one
+	 * relation on the inner side of the join.
+	 */
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/*
+	 * First let's query the unique and non-unique caches to see if we've
+	 * managed to prove that innerrel is unique for some subset of this
+	 * outerrel. We don't need an exact match, as if we have any extra
+	 * outerrels than were previously cached, then they can't make the
+	 * innerrel any less unique.
+	 */
+	foreach(lc, root->unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(unique_rels, outerrel->relids))
+			return true;		/* Success! */
+	}
+
+	/*
+	 * We may have previously determined that this outerrel, or some superset
+	 * thereof, cannot prove this innerrel to be unique.
+	 */
+	foreach(lc, root->non_unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(outerrel->relids, unique_rels))
+			return false;
+	}
+
+	/* No cached information, so try to make the proof. */
+	if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+	{
+		/*
+		 * Cache the positive result for future probes, being sure to keep it
+		 * in the planner_cxt even if we are working in GEQO.
+		 *
+		 * Note: one might consider trying to isolate the minimal subset of
+		 * the outerrels that proved the innerrel unique.  But it's not worth
+		 * the trouble, because the planner builds up joinrels incrementally
+		 * and so we'll see the minimally sufficient outerrels before any
+		 * supersets of them anyway.
+		 */
+		old_context = MemoryContextSwitchTo(root->planner_cxt);
+		root->unique_rels[innerrelid] =
+			lappend(root->unique_rels[innerrelid],
+					bms_copy(outerrel->relids));
+		MemoryContextSwitchTo(old_context);
+
+		return true;		/* Success! */
+	}
+	else
+	{
+		/*
+		 * None of the join conditions for outerrel proved innerrel unique, so
+		 * we can safely reject this outerrel or any subset of it in future
+		 * checks.
+		 *
+		 * However, in normal planning mode, caching this knowledge is totally
+		 * pointless; it won't be queried again, because we build up joinrels
+		 * from smaller to larger.  It is useful in GEQO mode, where the
+		 * knowledge can be carried across successive planning attempts; and
+		 * it's likely to be useful when using join-search plugins, too.
+		 * Hence cache only when join_search_private is non-NULL.  (Yeah,
+		 * that's a hack, but it seems reasonable.)
+		 */
+		if (root->join_search_private)
+		{
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->non_unique_rels[innerrelid] =
+				lappend(root->non_unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+
+		return false;
+	}
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index d002e6d..fd9993e 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -211,12 +211,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -231,7 +231,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3676,7 +3676,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -3978,7 +3978,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -4118,7 +4118,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5290,7 +5290,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5299,9 +5299,10 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5313,7 +5314,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5323,8 +5324,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5365,7 +5367,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5379,8 +5381,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 3c58d05..415acfc 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 8ce772d..c82ff39 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -2019,11 +2019,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -2034,16 +2032,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -2079,7 +2076,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2092,8 +2089,9 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2106,10 +2104,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2122,10 +2119,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2133,6 +2129,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2142,7 +2139,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2155,12 +2152,13 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2172,11 +2170,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2186,15 +2182,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2204,7 +2199,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2229,10 +2224,11 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index caf8291..65c92bf 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -81,6 +81,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f856f60..49d3e9d 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1760,6 +1760,9 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		first_inner_tuple_only; /* True if we should stop searching
+										 * for more inner tuples after finding
+										 * the first match */
 } JoinState;
 
 /* ----------------
@@ -1790,6 +1793,7 @@ typedef struct NestLoopState
  *		FillInner		   true if should emit unjoined inner tuples anyway
  *		MatchedOuter	   true if found a join match for current outer tuple
  *		MatchedInner	   true if found a join match for current inner tuple
+ *		SkipMarkRestore	   true if mark and restore are certainly no-ops
  *		OuterTupleSlot	   slot in tuple table for cur outer tuple
  *		InnerTupleSlot	   slot in tuple table for cur inner tuple
  *		MarkedTupleSlot    slot in tuple table for marked tuple
@@ -1814,6 +1818,7 @@ typedef struct MergeJoinState
 	bool		mj_FillInner;
 	bool		mj_MatchedOuter;
 	bool		mj_MatchedInner;
+	bool		mj_SkipMarkRestore;
 	TupleTableSlot *mj_OuterTupleSlot;
 	TupleTableSlot *mj_InnerTupleSlot;
 	TupleTableSlot *mj_MarkedTupleSlot;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index b880dc1..28638a2 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -615,6 +615,8 @@ typedef struct CustomScan
  * jointype:	rule for joining tuples from left and right subtrees
  * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
  *				(plan.qual contains conditions that came from WHERE)
+ * inner_unique	each outer tuple can match to no more than one inner
+ *				tuple
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -630,6 +632,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 05d6f07..59ceee1 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -222,6 +222,18 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner side based on the join condition.
+	 * This is a rather expensive test to perform, and requires looking for
+	 * proofs such as a relation's unique indexes.  We use this cache during
+	 * the join search to record lists of the sets of relations which both
+	 * prove, and disprove the uniqueness properties for the relid indexed by
+	 * these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1231,6 +1243,9 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* each outer tuple can match to no more than
+								 * one inner tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -2056,6 +2071,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique true if proofs exist which prove that outer side tuples match
+ *		no more than one inner side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2064,6 +2081,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index d9a9b12..ca33494 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -127,33 +127,29 @@ extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void initial_cost_mergejoin(PlannerInfo *root,
 					   JoinCostWorkspace *workspace,
 					   JoinType jointype,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo);
+					   JoinPathExtraData *extra);
 extern void final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo);
+					 JoinPathExtraData *extra);
 extern void initial_cost_hashjoin(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void cost_gather(GatherPath *path, PlannerInfo *root,
 			RelOptInfo *baserel, ParamPathInfo *param_info, double *rows);
 extern void cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 373c722..c5c4ac0 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -115,11 +115,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -127,10 +125,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -141,11 +138,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					  JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -251,6 +246,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..e37c647 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -105,7 +105,11 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
-
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist);
 /*
  * prototypes for plan/setrefs.c
  */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..9784a87 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,7 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +391,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -982,29 +983,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 564218b..a96b2a1 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -317,12 +317,11 @@ explain (costs off)
                      ->  Index Scan using ec1_expr2 on ec1 ec1_1
                      ->  Index Scan using ec1_expr3 on ec1 ec1_2
                      ->  Index Scan using ec1_expr4 on ec1 ec1_3
-               ->  Materialize
-                     ->  Sort
-                           Sort Key: ec1.f1 USING <
-                           ->  Index Scan using ec1_pkey on ec1
-                                 Index Cond: (ff = '42'::bigint)
-(20 rows)
+               ->  Sort
+                     Sort Key: ec1.f1 USING <
+                     ->  Index Scan using ec1_pkey on ec1
+                           Index Cond: (ff = '42'::bigint)
+(19 rows)
 
 -- check partially indexed scan
 set enable_nestloop = on;
@@ -374,12 +373,11 @@ explain (costs off)
                Sort Key: (((ec1_2.ff + 3) + 1))
                ->  Seq Scan on ec1 ec1_2
          ->  Index Scan using ec1_expr4 on ec1 ec1_3
-   ->  Materialize
-         ->  Sort
-               Sort Key: ec1.f1 USING <
-               ->  Index Scan using ec1_pkey on ec1
-                     Index Cond: (ff = '42'::bigint)
-(14 rows)
+   ->  Sort
+         Sort Key: ec1.f1 USING <
+         ->  Index Scan using ec1_pkey on ec1
+               Index Cond: (ff = '42'::bigint)
+(13 rows)
 
 -- check effects of row-level security
 set enable_nestloop = on;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 4992048..fe29353 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3370,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3414,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3426,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3446,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3479,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3491,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3516,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3550,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3562,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3590,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3622,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3642,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3672,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3688,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3713,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3736,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3762,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3778,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3793,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3823,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3850,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3938,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3951,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -3979,7 +4012,7 @@ select id from a where id in (
 );
          QUERY PLAN         
 ----------------------------
- Hash Semi Join
+ Hash Join
    Hash Cond: (a.id = b.id)
    ->  Seq Scan on a
    ->  Hash
@@ -4805,12 +4838,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4837,12 +4871,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4870,6 +4905,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4877,7 +4913,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4897,12 +4933,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4924,10 +4961,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4936,7 +4975,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4982,16 +5021,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5058,13 +5099,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5080,7 +5125,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5098,17 +5143,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5130,7 +5180,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5143,16 +5193,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5165,10 +5217,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5177,7 +5231,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5204,10 +5258,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5229,7 +5285,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5327,3 +5383,315 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop Left Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+                 QUERY PLAN                 
+--------------------------------------------
+ Merge Join
+   Merge Cond: (j1.id1 = j2.id1)
+   Join Filter: (j1.id2 = j2.id2)
+   ->  Index Scan using j1_id1_idx on j1
+   ->  Index Scan using j1_id1_idx on j1 j2
+(5 rows)
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+ id1 | id2 | id1 | id2 
+-----+-----+-----+-----
+   1 |   1 |   1 |   1
+   1 |   2 |   1 |   2
+(2 rows)
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 04848c1..e40bdf3 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,13 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(8 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 526a4ae..8df33be 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -1994,12 +1994,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2020,11 +2021,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index ed7d6d8..063cb13 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -837,6 +837,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -845,7 +846,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -865,6 +866,7 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -882,7 +884,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 3b7f689..3e4c3fb 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2195,6 +2195,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2202,6 +2203,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2209,6 +2211,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2216,12 +2219,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out
index d3bd8c9..7e0f3cf 100644
--- a/src/test/regress/expected/xml_1.out
+++ b/src/test/regress/expected/xml_1.out
@@ -926,12 +926,13 @@ EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1;
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- XMLNAMESPACES tests
 SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz),
@@ -1068,12 +1069,13 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- test qual
 SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) WHERE "COUNTRY_NAME" = 'Japan';
@@ -1087,13 +1089,14 @@ SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan"
 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
          Table Function Call: XMLTABLE(('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]'::text) PASSING (xmldata.data) COLUMNS "COUNTRY_NAME" text, "REGION_ID" integer)
          Filter: ("xmltable"."COUNTRY_NAME" = 'Japan'::text)
-(8 rows)
+(9 rows)
 
 -- should to work with more data
 INSERT INTO xmldata VALUES('<ROWS>
@@ -1186,13 +1189,14 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
          Filter: ("xmltable".region_id = 2)
-(8 rows)
+(9 rows)
 
 -- should fail, NULL value
 SELECT  xmltable.*
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index cca1a53..7118459 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1732,3 +1732,127 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+
+drop table j1;
+drop table j2;
+drop table j3;
#118David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#117)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 14 March 2017 at 16:37, David Rowley <david.rowley@2ndquadrant.com> wrote:

On 14 March 2017 at 11:35, David Rowley <david.rowley@2ndquadrant.com>
wrote:

On 14 March 2017 at 07:50, Tom Lane <tgl@sss.pgh.pa.us> wrote:

[ getting back to this patch finally... ]

David Rowley <david.rowley@2ndquadrant.com> writes:

I've attached a patch which implements this, though only for
MergeJoin, else I'd imagine we'd also need to ensure all proofs used
for testing the uniqueness were also hash-able too. I added some XXX
comments in analyzejoin.c around the mergeopfamilies == NIL tests to
mention that Merge Join depends on all the unique proof quals having
mergeopfamilies. This also assumes we'll never use some subset of
mergejoin-able quals for a merge join, which could be an interesting
area in the future, as we might have some btree index on a subset of
those columns to provide pre-sorted input. In short, it's a great
optimisation, but I'm a little scared we might break it one day.

Umm ... it's broken already isn't it? See the comment for
generate_mergejoin_paths:

Thanks for looking at this again.

Yeah confirmed. It's broken. I guess I just need to remember in the Path
if we got all the join quals, although I've not looked in detail what the
best fix is. I imagine it'll require storing something else in the JoinPath.

OK, so I've spent some more time on this and I've come up with a solution.

Basically the solution is to not skip mark and restore when joinquals
contains any items. This is a requirement for SEMI joins, but overly
cautious for unique joins. However, I think it'll apply in most cases when
Merge Join will be a win, and that's when there's a btree index on both
sides of the join which covers all columns in the join condition. I
carefully commented this part of the code to explain what can be done to
have it apply in more cases.

This caused me to go and change the following code too:

@@ -2676,6 +2688,9 @@ final_cost_mergejoin(PlannerInfo *root, MergePath
*path,
* it off does not entitle us to deliver an invalid plan.
*/
else if (innersortkeys == NIL &&
+ !((extra->inner_unique || path->jpath.jointype == JOIN_SEMI) &&
+ list_length(path->jpath.joinrestrictinfo) ==
+ list_length(path->path_mergeclauses)) &&
!ExecSupportsMarkRestore(inner_path))
path->materialize_inner = true;

I've been staring at this for a while and I think it's correct. If it's
wrong then we'd get "ERROR: unrecognized node type: <n>" from ExecMarkPos().

Here we must make sure and never skip materializing the inner side, if we're
not going to skip mark and restore in ExecInitMergeJoin().

I've attached a patch which fixes the issue.

Also added a regression test to try to make sure it stays fixed. There's a
few other mostly cosmetic fixes in there too.

Patch is attached which fixes up the conflict between the expression
evaluation performance patch.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-03-27.patchapplication/octet-stream; name=unique_joins_2017-03-27.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 1036b96..caf749e 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1329,6 +1329,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+				ExplainPropertyText("Inner Unique", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index f2c885a..c12fb26 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -288,10 +288,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NULL || ExecQual(otherqual, econtext))
@@ -399,6 +399,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -438,8 +446,13 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			hjstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 62784af..c400faf 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -802,10 +802,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NULL ||
@@ -826,7 +826,18 @@ ExecMergeJoin(MergeJoinState *node)
 						InstrCountFiltered2(node, 1);
 				}
 				else
+				{
+					/*
+					 * SkipMarkRestore will be set if we only require a single
+					 * match on the inner side, or if there can only be one
+					 * match. We can safely skip to the next outer tuple in
+					 * this case.
+					 */
+					if (node->mj_SkipMarkRestore)
+						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
+
 					InstrCountFiltered1(node, 1);
+				}
 				break;
 
 				/*
@@ -1048,7 +1059,9 @@ ExecMergeJoin(MergeJoinState *node)
 					/*
 					 * the merge clause matched so now we restore the inner
 					 * scan position to the first mark, and go join that tuple
-					 * (and any following ones) to the new outer.
+					 * (and any following ones) to the new outer.  If we were
+					 * able to determine mark and restore would be a no-op,
+					 * then we can skip this rigmarole.
 					 *
 					 * NOTE: we do not need to worry about the MatchedInner
 					 * state for the rescanned inner tuples.  We know all of
@@ -1062,17 +1075,20 @@ ExecMergeJoin(MergeJoinState *node)
 					 * forcing the merge clause to never match, so we never
 					 * get here.
 					 */
-					ExecRestrPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecRestrPos(innerPlan);
 
-					/*
-					 * ExecRestrPos probably should give us back a new Slot,
-					 * but since it doesn't, use the marked slot.  (The
-					 * previously returned mj_InnerTupleSlot cannot be assumed
-					 * to hold the required tuple.)
-					 */
-					node->mj_InnerTupleSlot = innerTupleSlot;
-					/* we need not do MJEvalInnerValues again */
+						/*
+						 * ExecRestrPos probably should give us back a new
+						 * Slot, but since it doesn't, use the marked slot.
+						 * (The previously returned mj_InnerTupleSlot cannot
+						 * be assumed to hold the required tuple.)
+						 */
+						node->mj_InnerTupleSlot = innerTupleSlot;
+					}
 
+					/* we need not do MJEvalInnerValues again */
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
@@ -1172,9 +1188,12 @@ ExecMergeJoin(MergeJoinState *node)
 
 				if (compareResult == 0)
 				{
-					ExecMarkPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecMarkPos(innerPlan);
 
-					MarkInnerTuple(node->mj_InnerTupleSlot, node);
+						MarkInnerTuple(node->mj_InnerTupleSlot, node);
+					}
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
@@ -1438,6 +1457,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1499,8 +1526,13 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			mergestate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
@@ -1581,6 +1613,36 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->mj_InnerTupleSlot = NULL;
 
 	/*
+	 * When matching to the first inner tuple only, as in the case with unique
+	 * and SEMI joins, if the joinqual list is empty (i.e all join conditions
+	 * are present in as merge join clauses), then when we find an inner tuple
+	 * matching the outer tuple, we've no need to keep scanning the inner side
+	 * looking for more tuples matching this outer tuple.  This means we're
+	 * never going to advance the inner side to check for more tuples which
+	 * match the current outer side tuple.  This basically boil down to, any
+	 * mark/restore operations we would perform would be a no-op.  The inner
+	 * side will already be in the correct location.  Here we detect this and
+	 * record that no mark and restore is required during the join.
+	 *
+	 * This mark and restore skipping can provide a good performance boost,
+	 * and it is a fairly common case, so it's certainly worth the cost of the
+	 * additional checks which must be performed during the join.
+	 *
+	 * We could apply this optimization in more casee for unique joins. We'd
+	 * just need to ensure that all of the quals which were used to prove the
+	 * inner side is unique are present in the merge join clauses. Currently
+	 * we've no way to get this information from the planner, but it may be
+	 * worth some exploration for ways to improve this in the future. Here we
+	 * can assume all the unique quals are present as merge join quals by the
+	 * lack of any joinquals.  Further tuning in this area is not possible for
+	 * SEMI joins.
+	 *
+	 * XXX if changing this see logic in final_cost_mergejoin()
+	 */
+	mergestate->mj_SkipMarkRestore = mergestate->js.joinqual == NIL &&
+									 mergestate->js.first_inner_tuple_only;
+
+	/*
 	 * initialization successful
 	 */
 	MJ1_printf("ExecInitMergeJoin: %s\n",
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 53977e0..ac5230c 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -219,10 +219,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need to join to the
+			 * first inner matching tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.first_inner_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NULL || ExecQual(otherqual, econtext))
@@ -273,6 +273,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -311,8 +319,13 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			nlstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 5722905..329cdc0 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2039,15 +2039,13 @@ cost_group(Path *path, PlannerInfo *root,
  * 'jointype' is the type of join to be performed
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2078,10 +2076,13 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -2114,17 +2115,16 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  *
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->outerjoinpath;
 	Path	   *inner_path = path->innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	Cost		startup_cost = workspace->startup_cost;
@@ -2164,10 +2164,13 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2308,7 +2311,7 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
  * 'inner_path' is the inner input to the join
  * 'outersortkeys' is the list of sort keys for the outer path
  * 'innersortkeys' is the list of sort keys for the inner path
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  *
  * Note: outersortkeys and innersortkeys should be NIL if no explicit
  * sort is needed because the respective source path is already ordered.
@@ -2319,7 +2322,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo)
+					   JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2396,7 +2399,8 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerstartsel = cache->leftstartsel;
 			innerendsel = cache->leftendsel;
 		}
-		if (jointype == JOIN_LEFT ||
+		if (extra->inner_unique ||
+			jointype == JOIN_LEFT ||
 			jointype == JOIN_ANTI)
 		{
 			outerstartsel = 0.0;
@@ -2534,12 +2538,12 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		materialize_inner
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo)
+					 JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
@@ -2628,9 +2632,14 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * computations?
 	 *
 	 * The whole issue is moot if we are working from a unique-ified outer
-	 * input.
+	 * input. We'll also never rescan during SEMI/ANTI/inner_unique as there's
+	 * no need to advance the inner side to find subsequent matches, therefore
+	 * no need to mark/restore.
 	 */
-	if (IsA(outer_path, UniquePath))
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		IsA(outer_path, UniquePath))
 		rescannedtuples = 0;
 	else
 	{
@@ -2679,7 +2688,11 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	/*
 	 * Even if materializing doesn't look cheaper, we *must* do it if the
 	 * inner path is to be used directly (without sorting) and it doesn't
-	 * support mark/restore.
+	 * support mark/restore. However, if the inner side of the join is unique
+	 * or we're performing a SEMI join, and all of the joinrestrictinfos are
+	 * present as merge clauses, then we may be able to skip mark and restore
+	 * completely.  The logic here must follow the logic that is used to set
+	 * mj_SkipMarkRestore in ExecInitMergeJoin().
 	 *
 	 * Since the inner side must be ordered, and only Sorts and IndexScans can
 	 * create order to begin with, and they both support mark/restore, you
@@ -2693,6 +2706,9 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * it off does not entitle us to deliver an invalid plan.
 	 */
 	else if (innersortkeys == NIL &&
+			 !((extra->inner_unique || path->jpath.jointype == JOIN_SEMI) &&
+			 list_length(path->jpath.joinrestrictinfo) ==
+			 list_length(path->path_mergeclauses)) &&
 			 !ExecSupportsMarkRestore(inner_path))
 		path->materialize_inner = true;
 
@@ -2834,16 +2850,14 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
  * 'hashclauses' is the list of joinclauses to be used as hash clauses
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2928,17 +2942,16 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	List	   *hashclauses = path->path_hashclauses;
@@ -3059,13 +3072,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index de7044d..ae89e89 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -21,6 +21,7 @@
 #include "optimizer/cost.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/planmain.h"
 
 /* Hook for plugins to get control in add_paths_to_joinrel() */
 set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
@@ -121,6 +122,13 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.
+	 */
+	extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+											jointype, sjinfo, restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -336,8 +344,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * methodology worthwhile.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -348,11 +355,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -399,8 +404,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
 
@@ -410,11 +414,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -486,7 +488,7 @@ try_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -497,10 +499,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -562,7 +563,7 @@ try_partial_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
@@ -573,10 +574,9 @@ try_partial_mergejoin_path(PlannerInfo *root,
 										   joinrel,
 										   jointype,
 										   &workspace,
-										   extra->sjinfo,
+										   extra,
 										   outer_path,
 										   inner_path,
-										   extra->restrictlist,
 										   pathkeys,
 										   NULL,
 										   mergeclauses,
@@ -620,8 +620,7 @@ try_hashjoin_path(PlannerInfo *root,
 	 * never have any output pathkeys, per comments in create_hashjoin_path.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -632,11 +631,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -683,8 +680,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, NIL))
 		return;
 
@@ -694,11 +690,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index ac63f75..361cec9 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -41,6 +41,10 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
 
 
 /*
@@ -845,3 +849,159 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' is unique based on the join
+ *	  condition in 'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist)
+{
+	MemoryContext	old_context;
+	ListCell	   *lc;
+	int				innerrelid;
+
+	/* can't prove uniqueness for joins with an empty restrictlist */
+	if (restrictlist == NIL)
+		return false;
+
+	/*
+	 * Currently we're unable to prove uniqueness when there's more than one
+	 * relation on the inner side of the join.
+	 */
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/*
+	 * First let's query the unique and non-unique caches to see if we've
+	 * managed to prove that innerrel is unique for some subset of this
+	 * outerrel. We don't need an exact match, as if we have any extra
+	 * outerrels than were previously cached, then they can't make the
+	 * innerrel any less unique.
+	 */
+	foreach(lc, root->unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(unique_rels, outerrel->relids))
+			return true;		/* Success! */
+	}
+
+	/*
+	 * We may have previously determined that this outerrel, or some superset
+	 * thereof, cannot prove this innerrel to be unique.
+	 */
+	foreach(lc, root->non_unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(outerrel->relids, unique_rels))
+			return false;
+	}
+
+	/* No cached information, so try to make the proof. */
+	if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+	{
+		/*
+		 * Cache the positive result for future probes, being sure to keep it
+		 * in the planner_cxt even if we are working in GEQO.
+		 *
+		 * Note: one might consider trying to isolate the minimal subset of
+		 * the outerrels that proved the innerrel unique.  But it's not worth
+		 * the trouble, because the planner builds up joinrels incrementally
+		 * and so we'll see the minimally sufficient outerrels before any
+		 * supersets of them anyway.
+		 */
+		old_context = MemoryContextSwitchTo(root->planner_cxt);
+		root->unique_rels[innerrelid] =
+			lappend(root->unique_rels[innerrelid],
+					bms_copy(outerrel->relids));
+		MemoryContextSwitchTo(old_context);
+
+		return true;		/* Success! */
+	}
+	else
+	{
+		/*
+		 * None of the join conditions for outerrel proved innerrel unique, so
+		 * we can safely reject this outerrel or any subset of it in future
+		 * checks.
+		 *
+		 * However, in normal planning mode, caching this knowledge is totally
+		 * pointless; it won't be queried again, because we build up joinrels
+		 * from smaller to larger.  It is useful in GEQO mode, where the
+		 * knowledge can be carried across successive planning attempts; and
+		 * it's likely to be useful when using join-search plugins, too.
+		 * Hence cache only when join_search_private is non-NULL.  (Yeah,
+		 * that's a hack, but it seems reasonable.)
+		 */
+		if (root->join_search_private)
+		{
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->non_unique_rels[innerrelid] =
+				lappend(root->non_unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+
+		return false;
+	}
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c80c999..d5fee77 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -211,12 +211,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -231,7 +231,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3652,7 +3652,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -3954,7 +3954,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -4094,7 +4094,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5267,7 +5267,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5276,9 +5276,10 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5290,7 +5291,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5300,8 +5301,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5342,7 +5344,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5356,8 +5358,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 3c58d05..415acfc 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index fca96eb..e1f637d 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -2022,11 +2022,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -2037,16 +2035,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -2082,7 +2079,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2095,8 +2092,9 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2109,10 +2107,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2125,10 +2122,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2136,6 +2132,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2145,7 +2142,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2158,12 +2155,13 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2175,11 +2173,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2189,15 +2185,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2207,7 +2202,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2232,10 +2227,11 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 6ab7854..cbf0582 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -84,6 +84,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index ff42895..a976b3e 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1516,6 +1516,9 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	ExprState  *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		first_inner_tuple_only; /* True if we should stop searching
+										 * for more inner tuples after finding
+										 * the first match */
 } JoinState;
 
 /* ----------------
@@ -1546,6 +1549,7 @@ typedef struct NestLoopState
  *		FillInner		   true if should emit unjoined inner tuples anyway
  *		MatchedOuter	   true if found a join match for current outer tuple
  *		MatchedInner	   true if found a join match for current inner tuple
+ *		SkipMarkRestore	   true if mark and restore are certainly no-ops
  *		OuterTupleSlot	   slot in tuple table for cur outer tuple
  *		InnerTupleSlot	   slot in tuple table for cur inner tuple
  *		MarkedTupleSlot    slot in tuple table for marked tuple
@@ -1570,6 +1574,7 @@ typedef struct MergeJoinState
 	bool		mj_FillInner;
 	bool		mj_MatchedOuter;
 	bool		mj_MatchedInner;
+	bool		mj_SkipMarkRestore;
 	TupleTableSlot *mj_OuterTupleSlot;
 	TupleTableSlot *mj_InnerTupleSlot;
 	TupleTableSlot *mj_MarkedTupleSlot;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 4a95e16..5ebeb7b 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -624,6 +624,8 @@ typedef struct CustomScan
  * jointype:	rule for joining tuples from left and right subtrees
  * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
  *				(plan.qual contains conditions that came from WHERE)
+ * inner_unique	each outer tuple can match to no more than one inner
+ *				tuple
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -639,6 +641,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 0a5187c..f6323b4 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -224,6 +224,18 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner side based on the join condition.
+	 * This is a rather expensive test to perform, and requires looking for
+	 * proofs such as a relation's unique indexes.  We use this cache during
+	 * the join search to record lists of the sets of relations which both
+	 * prove, and disprove the uniqueness properties for the relid indexed by
+	 * these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1258,6 +1270,9 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* each outer tuple can match to no more than
+								 * one inner tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -2104,6 +2119,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique true if proofs exist which prove that outer side tuples match
+ *		no more than one inner side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2112,6 +2129,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index d9a9b12..ca33494 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -127,33 +127,29 @@ extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void initial_cost_mergejoin(PlannerInfo *root,
 					   JoinCostWorkspace *workspace,
 					   JoinType jointype,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo);
+					   JoinPathExtraData *extra);
 extern void final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo);
+					 JoinPathExtraData *extra);
 extern void initial_cost_hashjoin(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void cost_gather(GatherPath *path, PlannerInfo *root,
 			RelOptInfo *baserel, ParamPathInfo *param_info, double *rows);
 extern void cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 81640de..9d229d0 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -117,11 +117,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -129,10 +127,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -143,11 +140,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					  JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -253,6 +248,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..e37c647 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -105,7 +105,11 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
-
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist);
 /*
  * prototypes for plan/setrefs.c
  */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..9784a87 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,7 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +391,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -982,29 +983,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 564218b..a96b2a1 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -317,12 +317,11 @@ explain (costs off)
                      ->  Index Scan using ec1_expr2 on ec1 ec1_1
                      ->  Index Scan using ec1_expr3 on ec1 ec1_2
                      ->  Index Scan using ec1_expr4 on ec1 ec1_3
-               ->  Materialize
-                     ->  Sort
-                           Sort Key: ec1.f1 USING <
-                           ->  Index Scan using ec1_pkey on ec1
-                                 Index Cond: (ff = '42'::bigint)
-(20 rows)
+               ->  Sort
+                     Sort Key: ec1.f1 USING <
+                     ->  Index Scan using ec1_pkey on ec1
+                           Index Cond: (ff = '42'::bigint)
+(19 rows)
 
 -- check partially indexed scan
 set enable_nestloop = on;
@@ -374,12 +373,11 @@ explain (costs off)
                Sort Key: (((ec1_2.ff + 3) + 1))
                ->  Seq Scan on ec1 ec1_2
          ->  Index Scan using ec1_expr4 on ec1 ec1_3
-   ->  Materialize
-         ->  Sort
-               Sort Key: ec1.f1 USING <
-               ->  Index Scan using ec1_pkey on ec1
-                     Index Cond: (ff = '42'::bigint)
-(14 rows)
+   ->  Sort
+         Sort Key: ec1.f1 USING <
+         ->  Index Scan using ec1_pkey on ec1
+               Index Cond: (ff = '42'::bigint)
+(13 rows)
 
 -- check effects of row-level security
 set enable_nestloop = on;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 4992048..fe29353 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3370,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3414,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3426,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3446,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3479,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3491,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3516,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3550,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3562,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3590,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3622,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3642,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3672,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3688,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3713,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3736,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3762,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3778,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3793,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3823,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3850,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3938,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3951,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -3979,7 +4012,7 @@ select id from a where id in (
 );
          QUERY PLAN         
 ----------------------------
- Hash Semi Join
+ Hash Join
    Hash Cond: (a.id = b.id)
    ->  Seq Scan on a
    ->  Hash
@@ -4805,12 +4838,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4837,12 +4871,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4870,6 +4905,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4877,7 +4913,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4897,12 +4933,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4924,10 +4961,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4936,7 +4975,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4982,16 +5021,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5058,13 +5099,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5080,7 +5125,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5098,17 +5143,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5130,7 +5180,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5143,16 +5193,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5165,10 +5217,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5177,7 +5231,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5204,10 +5258,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5229,7 +5285,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5327,3 +5383,315 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop Left Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+                 QUERY PLAN                 
+--------------------------------------------
+ Merge Join
+   Merge Cond: (j1.id1 = j2.id1)
+   Join Filter: (j1.id2 = j2.id2)
+   ->  Index Scan using j1_id1_idx on j1
+   ->  Index Scan using j1_id1_idx on j1 j2
+(5 rows)
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+ id1 | id2 | id1 | id2 
+-----+-----+-----+-----
+   1 |   1 |   1 |   1
+   1 |   2 |   1 |   2
+(2 rows)
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 04848c1..e40bdf3 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,13 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(8 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 526a4ae..8df33be 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -1994,12 +1994,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2020,11 +2021,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index ed7d6d8..063cb13 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -837,6 +837,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -845,7 +846,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -865,6 +866,7 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -882,7 +884,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 3b7f689..3e4c3fb 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2195,6 +2195,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2202,6 +2203,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2209,6 +2211,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2216,12 +2219,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out
index d3bd8c9..7e0f3cf 100644
--- a/src/test/regress/expected/xml_1.out
+++ b/src/test/regress/expected/xml_1.out
@@ -926,12 +926,13 @@ EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1;
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- XMLNAMESPACES tests
 SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz),
@@ -1068,12 +1069,13 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- test qual
 SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) WHERE "COUNTRY_NAME" = 'Japan';
@@ -1087,13 +1089,14 @@ SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan"
 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
          Table Function Call: XMLTABLE(('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]'::text) PASSING (xmldata.data) COLUMNS "COUNTRY_NAME" text, "REGION_ID" integer)
          Filter: ("xmltable"."COUNTRY_NAME" = 'Japan'::text)
-(8 rows)
+(9 rows)
 
 -- should to work with more data
 INSERT INTO xmldata VALUES('<ROWS>
@@ -1186,13 +1189,14 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
          Filter: ("xmltable".region_id = 2)
-(8 rows)
+(9 rows)
 
 -- should fail, NULL value
 SELECT  xmltable.*
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index cca1a53..7118459 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1732,3 +1732,127 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+
+drop table j1;
+drop table j2;
+drop table j3;
#119David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#118)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 27 March 2017 at 09:28, David Rowley <david.rowley@2ndquadrant.com> wrote:

Patch is attached which fixes up the conflict between the expression
evaluation performance patch.

Seems I forgot to commit locally before creating the patch... Here's
the actual patch I meant to attach earlier.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-03-27a.patchapplication/octet-stream; name=unique_joins_2017-03-27a.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 1036b96..caf749e 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1329,6 +1329,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+			{
+				const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+				ExplainPropertyText("Inner Unique", value, es);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index f2c885a..c12fb26 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -288,10 +288,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NULL || ExecQual(otherqual, econtext))
@@ -399,6 +399,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -438,8 +446,13 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			hjstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 62784af..c453bc6 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -802,10 +802,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NULL ||
@@ -826,7 +826,18 @@ ExecMergeJoin(MergeJoinState *node)
 						InstrCountFiltered2(node, 1);
 				}
 				else
+				{
+					/*
+					 * SkipMarkRestore will be set if we only require a single
+					 * match on the inner side, or if there can only be one
+					 * match. We can safely skip to the next outer tuple in
+					 * this case.
+					 */
+					if (node->mj_SkipMarkRestore)
+						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
+
 					InstrCountFiltered1(node, 1);
+				}
 				break;
 
 				/*
@@ -1048,7 +1059,9 @@ ExecMergeJoin(MergeJoinState *node)
 					/*
 					 * the merge clause matched so now we restore the inner
 					 * scan position to the first mark, and go join that tuple
-					 * (and any following ones) to the new outer.
+					 * (and any following ones) to the new outer.  If we were
+					 * able to determine mark and restore would be a no-op,
+					 * then we can skip this rigmarole.
 					 *
 					 * NOTE: we do not need to worry about the MatchedInner
 					 * state for the rescanned inner tuples.  We know all of
@@ -1062,17 +1075,20 @@ ExecMergeJoin(MergeJoinState *node)
 					 * forcing the merge clause to never match, so we never
 					 * get here.
 					 */
-					ExecRestrPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecRestrPos(innerPlan);
 
-					/*
-					 * ExecRestrPos probably should give us back a new Slot,
-					 * but since it doesn't, use the marked slot.  (The
-					 * previously returned mj_InnerTupleSlot cannot be assumed
-					 * to hold the required tuple.)
-					 */
-					node->mj_InnerTupleSlot = innerTupleSlot;
-					/* we need not do MJEvalInnerValues again */
+						/*
+						 * ExecRestrPos probably should give us back a new
+						 * Slot, but since it doesn't, use the marked slot.
+						 * (The previously returned mj_InnerTupleSlot cannot
+						 * be assumed to hold the required tuple.)
+						 */
+						node->mj_InnerTupleSlot = innerTupleSlot;
+					}
 
+					/* we need not do MJEvalInnerValues again */
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
@@ -1172,9 +1188,12 @@ ExecMergeJoin(MergeJoinState *node)
 
 				if (compareResult == 0)
 				{
-					ExecMarkPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecMarkPos(innerPlan);
 
-					MarkInnerTuple(node->mj_InnerTupleSlot, node);
+						MarkInnerTuple(node->mj_InnerTupleSlot, node);
+					}
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
@@ -1438,6 +1457,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1499,8 +1526,13 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			mergestate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
@@ -1581,6 +1613,36 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->mj_InnerTupleSlot = NULL;
 
 	/*
+	 * When matching to the first inner tuple only, as in the case with unique
+	 * and SEMI joins, if the joinqual list is empty (i.e all join conditions
+	 * are present in as merge join clauses), then when we find an inner tuple
+	 * matching the outer tuple, we've no need to keep scanning the inner side
+	 * looking for more tuples matching this outer tuple.  This means we're
+	 * never going to advance the inner side to check for more tuples which
+	 * match the current outer side tuple.  This basically boil down to, any
+	 * mark/restore operations we would perform would be a no-op.  The inner
+	 * side will already be in the correct location.  Here we detect this and
+	 * record that no mark and restore is required during the join.
+	 *
+	 * This mark and restore skipping can provide a good performance boost,
+	 * and it is a fairly common case, so it's certainly worth the cost of the
+	 * additional checks which must be performed during the join.
+	 *
+	 * We could apply this optimization in more casee for unique joins. We'd
+	 * just need to ensure that all of the quals which were used to prove the
+	 * inner side is unique are present in the merge join clauses. Currently
+	 * we've no way to get this information from the planner, but it may be
+	 * worth some exploration for ways to improve this in the future. Here we
+	 * can assume all the unique quals are present as merge join quals by the
+	 * lack of any joinquals.  Further tuning in this area is not possible for
+	 * SEMI joins.
+	 *
+	 * XXX if changing this see logic in final_cost_mergejoin()
+	 */
+	mergestate->mj_SkipMarkRestore = !mergestate->js.joinqual &&
+									 mergestate->js.first_inner_tuple_only;
+
+	/*
 	 * initialization successful
 	 */
 	MJ1_printf("ExecInitMergeJoin: %s\n",
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 53977e0..ac5230c 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -219,10 +219,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need to join to the
+			 * first inner matching tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.first_inner_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NULL || ExecQual(otherqual, econtext))
@@ -273,6 +273,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -311,8 +319,13 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			nlstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 5722905..329cdc0 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2039,15 +2039,13 @@ cost_group(Path *path, PlannerInfo *root,
  * 'jointype' is the type of join to be performed
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2078,10 +2076,13 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -2114,17 +2115,16 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  *
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->outerjoinpath;
 	Path	   *inner_path = path->innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	Cost		startup_cost = workspace->startup_cost;
@@ -2164,10 +2164,13 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2308,7 +2311,7 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
  * 'inner_path' is the inner input to the join
  * 'outersortkeys' is the list of sort keys for the outer path
  * 'innersortkeys' is the list of sort keys for the inner path
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  *
  * Note: outersortkeys and innersortkeys should be NIL if no explicit
  * sort is needed because the respective source path is already ordered.
@@ -2319,7 +2322,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo)
+					   JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2396,7 +2399,8 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerstartsel = cache->leftstartsel;
 			innerendsel = cache->leftendsel;
 		}
-		if (jointype == JOIN_LEFT ||
+		if (extra->inner_unique ||
+			jointype == JOIN_LEFT ||
 			jointype == JOIN_ANTI)
 		{
 			outerstartsel = 0.0;
@@ -2534,12 +2538,12 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		materialize_inner
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo)
+					 JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
@@ -2628,9 +2632,14 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * computations?
 	 *
 	 * The whole issue is moot if we are working from a unique-ified outer
-	 * input.
+	 * input. We'll also never rescan during SEMI/ANTI/inner_unique as there's
+	 * no need to advance the inner side to find subsequent matches, therefore
+	 * no need to mark/restore.
 	 */
-	if (IsA(outer_path, UniquePath))
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		IsA(outer_path, UniquePath))
 		rescannedtuples = 0;
 	else
 	{
@@ -2679,7 +2688,11 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	/*
 	 * Even if materializing doesn't look cheaper, we *must* do it if the
 	 * inner path is to be used directly (without sorting) and it doesn't
-	 * support mark/restore.
+	 * support mark/restore. However, if the inner side of the join is unique
+	 * or we're performing a SEMI join, and all of the joinrestrictinfos are
+	 * present as merge clauses, then we may be able to skip mark and restore
+	 * completely.  The logic here must follow the logic that is used to set
+	 * mj_SkipMarkRestore in ExecInitMergeJoin().
 	 *
 	 * Since the inner side must be ordered, and only Sorts and IndexScans can
 	 * create order to begin with, and they both support mark/restore, you
@@ -2693,6 +2706,9 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * it off does not entitle us to deliver an invalid plan.
 	 */
 	else if (innersortkeys == NIL &&
+			 !((extra->inner_unique || path->jpath.jointype == JOIN_SEMI) &&
+			 list_length(path->jpath.joinrestrictinfo) ==
+			 list_length(path->path_mergeclauses)) &&
 			 !ExecSupportsMarkRestore(inner_path))
 		path->materialize_inner = true;
 
@@ -2834,16 +2850,14 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
  * 'hashclauses' is the list of joinclauses to be used as hash clauses
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2928,17 +2942,16 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	List	   *hashclauses = path->path_hashclauses;
@@ -3059,13 +3072,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index de7044d..ae89e89 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -21,6 +21,7 @@
 #include "optimizer/cost.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/planmain.h"
 
 /* Hook for plugins to get control in add_paths_to_joinrel() */
 set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
@@ -121,6 +122,13 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.
+	 */
+	extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+											jointype, sjinfo, restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -336,8 +344,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * methodology worthwhile.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -348,11 +355,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -399,8 +404,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
 
@@ -410,11 +414,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -486,7 +488,7 @@ try_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -497,10 +499,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -562,7 +563,7 @@ try_partial_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
@@ -573,10 +574,9 @@ try_partial_mergejoin_path(PlannerInfo *root,
 										   joinrel,
 										   jointype,
 										   &workspace,
-										   extra->sjinfo,
+										   extra,
 										   outer_path,
 										   inner_path,
-										   extra->restrictlist,
 										   pathkeys,
 										   NULL,
 										   mergeclauses,
@@ -620,8 +620,7 @@ try_hashjoin_path(PlannerInfo *root,
 	 * never have any output pathkeys, per comments in create_hashjoin_path.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -632,11 +631,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -683,8 +680,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, NIL))
 		return;
 
@@ -694,11 +690,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index ac63f75..361cec9 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -41,6 +41,10 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
 
 
 /*
@@ -845,3 +849,159 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' is unique based on the join
+ *	  condition in 'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist)
+{
+	MemoryContext	old_context;
+	ListCell	   *lc;
+	int				innerrelid;
+
+	/* can't prove uniqueness for joins with an empty restrictlist */
+	if (restrictlist == NIL)
+		return false;
+
+	/*
+	 * Currently we're unable to prove uniqueness when there's more than one
+	 * relation on the inner side of the join.
+	 */
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/*
+	 * First let's query the unique and non-unique caches to see if we've
+	 * managed to prove that innerrel is unique for some subset of this
+	 * outerrel. We don't need an exact match, as if we have any extra
+	 * outerrels than were previously cached, then they can't make the
+	 * innerrel any less unique.
+	 */
+	foreach(lc, root->unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(unique_rels, outerrel->relids))
+			return true;		/* Success! */
+	}
+
+	/*
+	 * We may have previously determined that this outerrel, or some superset
+	 * thereof, cannot prove this innerrel to be unique.
+	 */
+	foreach(lc, root->non_unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(outerrel->relids, unique_rels))
+			return false;
+	}
+
+	/* No cached information, so try to make the proof. */
+	if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+	{
+		/*
+		 * Cache the positive result for future probes, being sure to keep it
+		 * in the planner_cxt even if we are working in GEQO.
+		 *
+		 * Note: one might consider trying to isolate the minimal subset of
+		 * the outerrels that proved the innerrel unique.  But it's not worth
+		 * the trouble, because the planner builds up joinrels incrementally
+		 * and so we'll see the minimally sufficient outerrels before any
+		 * supersets of them anyway.
+		 */
+		old_context = MemoryContextSwitchTo(root->planner_cxt);
+		root->unique_rels[innerrelid] =
+			lappend(root->unique_rels[innerrelid],
+					bms_copy(outerrel->relids));
+		MemoryContextSwitchTo(old_context);
+
+		return true;		/* Success! */
+	}
+	else
+	{
+		/*
+		 * None of the join conditions for outerrel proved innerrel unique, so
+		 * we can safely reject this outerrel or any subset of it in future
+		 * checks.
+		 *
+		 * However, in normal planning mode, caching this knowledge is totally
+		 * pointless; it won't be queried again, because we build up joinrels
+		 * from smaller to larger.  It is useful in GEQO mode, where the
+		 * knowledge can be carried across successive planning attempts; and
+		 * it's likely to be useful when using join-search plugins, too.
+		 * Hence cache only when join_search_private is non-NULL.  (Yeah,
+		 * that's a hack, but it seems reasonable.)
+		 */
+		if (root->join_search_private)
+		{
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->non_unique_rels[innerrelid] =
+				lappend(root->non_unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+
+		return false;
+	}
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c80c999..d5fee77 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -211,12 +211,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -231,7 +231,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3652,7 +3652,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -3954,7 +3954,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -4094,7 +4094,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5267,7 +5267,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5276,9 +5276,10 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5290,7 +5291,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5300,8 +5301,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5342,7 +5344,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5356,8 +5358,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 3c58d05..415acfc 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index fca96eb..e1f637d 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -2022,11 +2022,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -2037,16 +2035,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -2082,7 +2079,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2095,8 +2092,9 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2109,10 +2107,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2125,10 +2122,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2136,6 +2132,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2145,7 +2142,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2158,12 +2155,13 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2175,11 +2173,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2189,15 +2185,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2207,7 +2202,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2232,10 +2227,11 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 6ab7854..cbf0582 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -84,6 +84,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index ff42895..a976b3e 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1516,6 +1516,9 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	ExprState  *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		first_inner_tuple_only; /* True if we should stop searching
+										 * for more inner tuples after finding
+										 * the first match */
 } JoinState;
 
 /* ----------------
@@ -1546,6 +1549,7 @@ typedef struct NestLoopState
  *		FillInner		   true if should emit unjoined inner tuples anyway
  *		MatchedOuter	   true if found a join match for current outer tuple
  *		MatchedInner	   true if found a join match for current inner tuple
+ *		SkipMarkRestore	   true if mark and restore are certainly no-ops
  *		OuterTupleSlot	   slot in tuple table for cur outer tuple
  *		InnerTupleSlot	   slot in tuple table for cur inner tuple
  *		MarkedTupleSlot    slot in tuple table for marked tuple
@@ -1570,6 +1574,7 @@ typedef struct MergeJoinState
 	bool		mj_FillInner;
 	bool		mj_MatchedOuter;
 	bool		mj_MatchedInner;
+	bool		mj_SkipMarkRestore;
 	TupleTableSlot *mj_OuterTupleSlot;
 	TupleTableSlot *mj_InnerTupleSlot;
 	TupleTableSlot *mj_MarkedTupleSlot;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 4a95e16..5ebeb7b 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -624,6 +624,8 @@ typedef struct CustomScan
  * jointype:	rule for joining tuples from left and right subtrees
  * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
  *				(plan.qual contains conditions that came from WHERE)
+ * inner_unique	each outer tuple can match to no more than one inner
+ *				tuple
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -639,6 +641,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 0a5187c..f6323b4 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -224,6 +224,18 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner side based on the join condition.
+	 * This is a rather expensive test to perform, and requires looking for
+	 * proofs such as a relation's unique indexes.  We use this cache during
+	 * the join search to record lists of the sets of relations which both
+	 * prove, and disprove the uniqueness properties for the relid indexed by
+	 * these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1258,6 +1270,9 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* each outer tuple can match to no more than
+								 * one inner tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -2104,6 +2119,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique true if proofs exist which prove that outer side tuples match
+ *		no more than one inner side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2112,6 +2129,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index d9a9b12..ca33494 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -127,33 +127,29 @@ extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void initial_cost_mergejoin(PlannerInfo *root,
 					   JoinCostWorkspace *workspace,
 					   JoinType jointype,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo);
+					   JoinPathExtraData *extra);
 extern void final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo);
+					 JoinPathExtraData *extra);
 extern void initial_cost_hashjoin(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void cost_gather(GatherPath *path, PlannerInfo *root,
 			RelOptInfo *baserel, ParamPathInfo *param_info, double *rows);
 extern void cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 81640de..9d229d0 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -117,11 +117,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -129,10 +127,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -143,11 +140,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					  JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -253,6 +248,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..e37c647 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -105,7 +105,11 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
-
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist);
 /*
  * prototypes for plan/setrefs.c
  */
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..9784a87 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,7 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +391,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -982,29 +983,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 564218b..a96b2a1 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -317,12 +317,11 @@ explain (costs off)
                      ->  Index Scan using ec1_expr2 on ec1 ec1_1
                      ->  Index Scan using ec1_expr3 on ec1 ec1_2
                      ->  Index Scan using ec1_expr4 on ec1 ec1_3
-               ->  Materialize
-                     ->  Sort
-                           Sort Key: ec1.f1 USING <
-                           ->  Index Scan using ec1_pkey on ec1
-                                 Index Cond: (ff = '42'::bigint)
-(20 rows)
+               ->  Sort
+                     Sort Key: ec1.f1 USING <
+                     ->  Index Scan using ec1_pkey on ec1
+                           Index Cond: (ff = '42'::bigint)
+(19 rows)
 
 -- check partially indexed scan
 set enable_nestloop = on;
@@ -374,12 +373,11 @@ explain (costs off)
                Sort Key: (((ec1_2.ff + 3) + 1))
                ->  Seq Scan on ec1 ec1_2
          ->  Index Scan using ec1_expr4 on ec1 ec1_3
-   ->  Materialize
-         ->  Sort
-               Sort Key: ec1.f1 USING <
-               ->  Index Scan using ec1_pkey on ec1
-                     Index Cond: (ff = '42'::bigint)
-(14 rows)
+   ->  Sort
+         Sort Key: ec1.f1 USING <
+         ->  Index Scan using ec1_pkey on ec1
+               Index Cond: (ff = '42'::bigint)
+(13 rows)
 
 -- check effects of row-level security
 set enable_nestloop = on;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 4992048..fe29353 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3370,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3414,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3426,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3446,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3479,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3491,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3516,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3550,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3562,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3590,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3622,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3642,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3672,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3688,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3713,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3736,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3762,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3778,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3793,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3823,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3850,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3938,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3951,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -3979,7 +4012,7 @@ select id from a where id in (
 );
          QUERY PLAN         
 ----------------------------
- Hash Semi Join
+ Hash Join
    Hash Cond: (a.id = b.id)
    ->  Seq Scan on a
    ->  Hash
@@ -4805,12 +4838,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4837,12 +4871,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4870,6 +4905,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4877,7 +4913,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4897,12 +4933,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4924,10 +4961,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4936,7 +4975,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4982,16 +5021,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5058,13 +5099,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5080,7 +5125,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5098,17 +5143,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5130,7 +5180,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5143,16 +5193,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5165,10 +5217,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5177,7 +5231,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5204,10 +5258,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5229,7 +5285,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5327,3 +5383,315 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop Left Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+                 QUERY PLAN                 
+--------------------------------------------
+ Merge Join
+   Merge Cond: (j1.id1 = j2.id1)
+   Join Filter: (j1.id2 = j2.id2)
+   ->  Index Scan using j1_id1_idx on j1
+   ->  Index Scan using j1_id1_idx on j1 j2
+(5 rows)
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+ id1 | id2 | id1 | id2 
+-----+-----+-----+-----
+   1 |   1 |   1 |   1
+   1 |   2 |   1 |   2
+(2 rows)
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 04848c1..e40bdf3 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,13 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(8 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 526a4ae..8df33be 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -1994,12 +1994,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2020,11 +2021,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index ed7d6d8..063cb13 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -837,6 +837,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -845,7 +846,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -865,6 +866,7 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -882,7 +884,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 3b7f689..3e4c3fb 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2195,6 +2195,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2202,6 +2203,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2209,6 +2211,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2216,12 +2219,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out
index d3bd8c9..7e0f3cf 100644
--- a/src/test/regress/expected/xml_1.out
+++ b/src/test/regress/expected/xml_1.out
@@ -926,12 +926,13 @@ EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1;
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- XMLNAMESPACES tests
 SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz),
@@ -1068,12 +1069,13 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- test qual
 SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) WHERE "COUNTRY_NAME" = 'Japan';
@@ -1087,13 +1089,14 @@ SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan"
 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
          Table Function Call: XMLTABLE(('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]'::text) PASSING (xmldata.data) COLUMNS "COUNTRY_NAME" text, "REGION_ID" integer)
          Filter: ("xmltable"."COUNTRY_NAME" = 'Japan'::text)
-(8 rows)
+(9 rows)
 
 -- should to work with more data
 INSERT INTO xmldata VALUES('<ROWS>
@@ -1186,13 +1189,14 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
          Filter: ("xmltable".region_id = 2)
-(8 rows)
+(9 rows)
 
 -- should fail, NULL value
 SELECT  xmltable.*
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index cca1a53..7118459 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1732,3 +1732,127 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+
+drop table j1;
+drop table j2;
+drop table j3;
#120David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#119)
Re: Performance improvement for joins where outer side is unique

On 27 March 2017 at 15:51, David Rowley <david.rowley@2ndquadrant.com>
wrote:

On 27 March 2017 at 09:28, David Rowley <david.rowley@2ndquadrant.com>
wrote:

Patch is attached which fixes up the conflict between the expression
evaluation performance patch.

Seems I forgot to commit locally before creating the patch... Here's
the actual patch I meant to attach earlier.

I've attached an updated patch which updates the regression test output of
a recent commit to include the "Unique Inner" in the expected results.

I've also performed a pgindent run on the patch.

Tom, I'm wondering if you think you'll get time to look at this before the
feature freeze?

David

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#121Robert Haas
robertmhaas@gmail.com
In reply to: David Rowley (#120)
Re: Performance improvement for joins where outer side is unique

On Sun, Apr 2, 2017 at 5:21 AM, David Rowley
<david.rowley@2ndquadrant.com> wrote:

I've attached an updated patch which updates the regression test output of a
recent commit to include the "Unique Inner" in the expected results.

Was this email supposed to have a patch attached?

Tom, I'm wondering if you think you'll get time to look at this before the
feature freeze?

/me crosses fingers.

--
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

#122David Rowley
david.rowley@2ndquadrant.com
In reply to: David Rowley (#120)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 2 April 2017 at 21:21, David Rowley <david.rowley@2ndquadrant.com> wrote:

I've attached an updated patch which updates the regression test output of
a recent commit to include the "Unique Inner" in the expected results.

The patch must've fallen off. Attempt number 2 at attaching.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-04-02.patchapplication/octet-stream; name=unique_joins_2017-04-02.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a18ab43..009d982 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1343,6 +1343,25 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+				{
+					const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+
+					ExplainPropertyText("Inner Unique", value, es);
+					break;
+				}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index f2c885a..b73cb90 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -288,10 +288,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NULL || ExecQual(otherqual, econtext))
@@ -399,6 +399,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -438,8 +446,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			hjstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 62784af..4a2a38d 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -802,10 +802,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NULL ||
@@ -826,7 +826,18 @@ ExecMergeJoin(MergeJoinState *node)
 						InstrCountFiltered2(node, 1);
 				}
 				else
+				{
+					/*
+					 * SkipMarkRestore will be set if we only require a single
+					 * match on the inner side, or if there can only be one
+					 * match. We can safely skip to the next outer tuple in
+					 * this case.
+					 */
+					if (node->mj_SkipMarkRestore)
+						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
+
 					InstrCountFiltered1(node, 1);
+				}
 				break;
 
 				/*
@@ -1048,7 +1059,9 @@ ExecMergeJoin(MergeJoinState *node)
 					/*
 					 * the merge clause matched so now we restore the inner
 					 * scan position to the first mark, and go join that tuple
-					 * (and any following ones) to the new outer.
+					 * (and any following ones) to the new outer.  If we were
+					 * able to determine mark and restore would be a no-op,
+					 * then we can skip this rigmarole.
 					 *
 					 * NOTE: we do not need to worry about the MatchedInner
 					 * state for the rescanned inner tuples.  We know all of
@@ -1062,17 +1075,20 @@ ExecMergeJoin(MergeJoinState *node)
 					 * forcing the merge clause to never match, so we never
 					 * get here.
 					 */
-					ExecRestrPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecRestrPos(innerPlan);
 
-					/*
-					 * ExecRestrPos probably should give us back a new Slot,
-					 * but since it doesn't, use the marked slot.  (The
-					 * previously returned mj_InnerTupleSlot cannot be assumed
-					 * to hold the required tuple.)
-					 */
-					node->mj_InnerTupleSlot = innerTupleSlot;
-					/* we need not do MJEvalInnerValues again */
+						/*
+						 * ExecRestrPos probably should give us back a new
+						 * Slot, but since it doesn't, use the marked slot.
+						 * (The previously returned mj_InnerTupleSlot cannot
+						 * be assumed to hold the required tuple.)
+						 */
+						node->mj_InnerTupleSlot = innerTupleSlot;
+					}
 
+					/* we need not do MJEvalInnerValues again */
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
@@ -1172,9 +1188,12 @@ ExecMergeJoin(MergeJoinState *node)
 
 				if (compareResult == 0)
 				{
-					ExecMarkPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecMarkPos(innerPlan);
 
-					MarkInnerTuple(node->mj_InnerTupleSlot, node);
+						MarkInnerTuple(node->mj_InnerTupleSlot, node);
+					}
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
@@ -1438,6 +1457,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1499,8 +1526,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			mergestate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
@@ -1581,6 +1614,36 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->mj_InnerTupleSlot = NULL;
 
 	/*
+	 * When matching to the first inner tuple only, as in the case with unique
+	 * and SEMI joins, if the joinqual list is empty (i.e all join conditions
+	 * are present in as merge join clauses), then when we find an inner tuple
+	 * matching the outer tuple, we've no need to keep scanning the inner side
+	 * looking for more tuples matching this outer tuple.  This means we're
+	 * never going to advance the inner side to check for more tuples which
+	 * match the current outer side tuple.  This basically boil down to, any
+	 * mark/restore operations we would perform would be a no-op.  The inner
+	 * side will already be in the correct location.  Here we detect this and
+	 * record that no mark and restore is required during the join.
+	 *
+	 * This mark and restore skipping can provide a good performance boost,
+	 * and it is a fairly common case, so it's certainly worth the cost of the
+	 * additional checks which must be performed during the join.
+	 *
+	 * We could apply this optimization in more casee for unique joins. We'd
+	 * just need to ensure that all of the quals which were used to prove the
+	 * inner side is unique are present in the merge join clauses. Currently
+	 * we've no way to get this information from the planner, but it may be
+	 * worth some exploration for ways to improve this in the future. Here we
+	 * can assume all the unique quals are present as merge join quals by the
+	 * lack of any joinquals.  Further tuning in this area is not possible for
+	 * SEMI joins.
+	 *
+	 * XXX if changing this see logic in final_cost_mergejoin()
+	 */
+	mergestate->mj_SkipMarkRestore = !mergestate->js.joinqual &&
+		mergestate->js.first_inner_tuple_only;
+
+	/*
 	 * initialization successful
 	 */
 	MJ1_printf("ExecInitMergeJoin: %s\n",
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 53977e0..bd16af1 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -219,10 +219,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need to join to the
+			 * first inner matching tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.first_inner_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NULL || ExecQual(otherqual, econtext))
@@ -273,6 +273,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -311,8 +319,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			nlstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index ed07e2f..f7af7b4 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2081,15 +2081,13 @@ cost_group(Path *path, PlannerInfo *root,
  * 'jointype' is the type of join to be performed
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2120,10 +2118,13 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -2156,17 +2157,16 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  *
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->outerjoinpath;
 	Path	   *inner_path = path->innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	Cost		startup_cost = workspace->startup_cost;
@@ -2206,10 +2206,13 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2350,7 +2353,7 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
  * 'inner_path' is the inner input to the join
  * 'outersortkeys' is the list of sort keys for the outer path
  * 'innersortkeys' is the list of sort keys for the inner path
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  *
  * Note: outersortkeys and innersortkeys should be NIL if no explicit
  * sort is needed because the respective source path is already ordered.
@@ -2361,7 +2364,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo)
+					   JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2438,7 +2441,8 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerstartsel = cache->leftstartsel;
 			innerendsel = cache->leftendsel;
 		}
-		if (jointype == JOIN_LEFT ||
+		if (extra->inner_unique ||
+			jointype == JOIN_LEFT ||
 			jointype == JOIN_ANTI)
 		{
 			outerstartsel = 0.0;
@@ -2576,12 +2580,12 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		materialize_inner
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo)
+					 JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
@@ -2670,9 +2674,14 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * computations?
 	 *
 	 * The whole issue is moot if we are working from a unique-ified outer
-	 * input.
+	 * input. We'll also never rescan during SEMI/ANTI/inner_unique as there's
+	 * no need to advance the inner side to find subsequent matches, therefore
+	 * no need to mark/restore.
 	 */
-	if (IsA(outer_path, UniquePath))
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		IsA(outer_path, UniquePath))
 		rescannedtuples = 0;
 	else
 	{
@@ -2721,7 +2730,11 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	/*
 	 * Even if materializing doesn't look cheaper, we *must* do it if the
 	 * inner path is to be used directly (without sorting) and it doesn't
-	 * support mark/restore.
+	 * support mark/restore. However, if the inner side of the join is unique
+	 * or we're performing a SEMI join, and all of the joinrestrictinfos are
+	 * present as merge clauses, then we may be able to skip mark and restore
+	 * completely.  The logic here must follow the logic that is used to set
+	 * mj_SkipMarkRestore in ExecInitMergeJoin().
 	 *
 	 * Since the inner side must be ordered, and only Sorts and IndexScans can
 	 * create order to begin with, and they both support mark/restore, you
@@ -2735,6 +2748,9 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * it off does not entitle us to deliver an invalid plan.
 	 */
 	else if (innersortkeys == NIL &&
+			 !((extra->inner_unique || path->jpath.jointype == JOIN_SEMI) &&
+			   list_length(path->jpath.joinrestrictinfo) ==
+			   list_length(path->path_mergeclauses)) &&
 			 !ExecSupportsMarkRestore(inner_path))
 		path->materialize_inner = true;
 
@@ -2876,16 +2892,14 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
  * 'hashclauses' is the list of joinclauses to be used as hash clauses
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2970,17 +2984,16 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	List	   *hashclauses = path->path_hashclauses;
@@ -3101,13 +3114,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index de7044d..ae89e89 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -21,6 +21,7 @@
 #include "optimizer/cost.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/planmain.h"
 
 /* Hook for plugins to get control in add_paths_to_joinrel() */
 set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
@@ -121,6 +122,13 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.
+	 */
+	extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+											jointype, sjinfo, restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -336,8 +344,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * methodology worthwhile.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -348,11 +355,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -399,8 +404,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
 
@@ -410,11 +414,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -486,7 +488,7 @@ try_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -497,10 +499,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -562,7 +563,7 @@ try_partial_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
@@ -573,10 +574,9 @@ try_partial_mergejoin_path(PlannerInfo *root,
 										   joinrel,
 										   jointype,
 										   &workspace,
-										   extra->sjinfo,
+										   extra,
 										   outer_path,
 										   inner_path,
-										   extra->restrictlist,
 										   pathkeys,
 										   NULL,
 										   mergeclauses,
@@ -620,8 +620,7 @@ try_hashjoin_path(PlannerInfo *root,
 	 * never have any output pathkeys, per comments in create_hashjoin_path.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -632,11 +631,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -683,8 +680,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, NIL))
 		return;
 
@@ -694,11 +690,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index ac63f75..361cec9 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -41,6 +41,10 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
 
 
 /*
@@ -845,3 +849,159 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' is unique based on the join
+ *	  condition in 'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist)
+{
+	MemoryContext	old_context;
+	ListCell	   *lc;
+	int				innerrelid;
+
+	/* can't prove uniqueness for joins with an empty restrictlist */
+	if (restrictlist == NIL)
+		return false;
+
+	/*
+	 * Currently we're unable to prove uniqueness when there's more than one
+	 * relation on the inner side of the join.
+	 */
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/*
+	 * First let's query the unique and non-unique caches to see if we've
+	 * managed to prove that innerrel is unique for some subset of this
+	 * outerrel. We don't need an exact match, as if we have any extra
+	 * outerrels than were previously cached, then they can't make the
+	 * innerrel any less unique.
+	 */
+	foreach(lc, root->unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(unique_rels, outerrel->relids))
+			return true;		/* Success! */
+	}
+
+	/*
+	 * We may have previously determined that this outerrel, or some superset
+	 * thereof, cannot prove this innerrel to be unique.
+	 */
+	foreach(lc, root->non_unique_rels[innerrelid])
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(outerrel->relids, unique_rels))
+			return false;
+	}
+
+	/* No cached information, so try to make the proof. */
+	if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+	{
+		/*
+		 * Cache the positive result for future probes, being sure to keep it
+		 * in the planner_cxt even if we are working in GEQO.
+		 *
+		 * Note: one might consider trying to isolate the minimal subset of
+		 * the outerrels that proved the innerrel unique.  But it's not worth
+		 * the trouble, because the planner builds up joinrels incrementally
+		 * and so we'll see the minimally sufficient outerrels before any
+		 * supersets of them anyway.
+		 */
+		old_context = MemoryContextSwitchTo(root->planner_cxt);
+		root->unique_rels[innerrelid] =
+			lappend(root->unique_rels[innerrelid],
+					bms_copy(outerrel->relids));
+		MemoryContextSwitchTo(old_context);
+
+		return true;		/* Success! */
+	}
+	else
+	{
+		/*
+		 * None of the join conditions for outerrel proved innerrel unique, so
+		 * we can safely reject this outerrel or any subset of it in future
+		 * checks.
+		 *
+		 * However, in normal planning mode, caching this knowledge is totally
+		 * pointless; it won't be queried again, because we build up joinrels
+		 * from smaller to larger.  It is useful in GEQO mode, where the
+		 * knowledge can be carried across successive planning attempts; and
+		 * it's likely to be useful when using join-search plugins, too.
+		 * Hence cache only when join_search_private is non-NULL.  (Yeah,
+		 * that's a hack, but it seems reasonable.)
+		 */
+		if (root->join_search_private)
+		{
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->non_unique_rels[innerrelid] =
+				lappend(root->non_unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+
+		return false;
+	}
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 2a78595..0ca8dfb 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -215,12 +215,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -235,7 +235,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3714,7 +3714,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -4016,7 +4016,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -4156,7 +4156,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5349,7 +5349,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5358,9 +5358,10 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5372,7 +5373,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5382,8 +5383,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5424,7 +5426,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5438,8 +5440,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 3c58d05..415acfc 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 8536212..a2875dc 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -2049,11 +2049,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -2064,16 +2062,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -2109,7 +2106,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2122,8 +2119,9 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2136,10 +2134,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2152,10 +2149,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2163,6 +2159,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2172,7 +2169,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2185,12 +2182,13 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2202,11 +2200,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2216,15 +2212,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2234,7 +2229,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2259,10 +2254,11 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 7912df0..83da425 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -84,6 +84,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = list_length(root->parse->rtable) + 1;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index fa99244..66ed681 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1537,6 +1537,9 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	ExprState  *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		first_inner_tuple_only; /* True if we should stop searching
+										 * for more inner tuples after finding
+										 * the first match */
 } JoinState;
 
 /* ----------------
@@ -1567,6 +1570,7 @@ typedef struct NestLoopState
  *		FillInner		   true if should emit unjoined inner tuples anyway
  *		MatchedOuter	   true if found a join match for current outer tuple
  *		MatchedInner	   true if found a join match for current inner tuple
+ *		SkipMarkRestore    true if mark and restore are certainly no-ops
  *		OuterTupleSlot	   slot in tuple table for cur outer tuple
  *		InnerTupleSlot	   slot in tuple table for cur inner tuple
  *		MarkedTupleSlot    slot in tuple table for marked tuple
@@ -1591,6 +1595,7 @@ typedef struct MergeJoinState
 	bool		mj_FillInner;
 	bool		mj_MatchedOuter;
 	bool		mj_MatchedInner;
+	bool		mj_SkipMarkRestore;
 	TupleTableSlot *mj_OuterTupleSlot;
 	TupleTableSlot *mj_InnerTupleSlot;
 	TupleTableSlot *mj_MarkedTupleSlot;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index a2dd26f..b16d04c 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -634,6 +634,8 @@ typedef struct CustomScan
  * jointype:	rule for joining tuples from left and right subtrees
  * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
  *				(plan.qual contains conditions that came from WHERE)
+ * inner_unique each outer tuple can match to no more than one inner
+ *				tuple
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -649,6 +651,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index ebf9480..38b61f6 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -224,6 +224,18 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner side based on the join condition.
+	 * This is a rather expensive test to perform, and requires looking for
+	 * proofs such as a relation's unique indexes.  We use this cache during
+	 * the join search to record lists of the sets of relations which both
+	 * prove, and disprove the uniqueness properties for the relid indexed by
+	 * these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -1259,6 +1271,9 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* each outer tuple can match to no more than
+								 * one inner tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -2125,6 +2140,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique true if proofs exist which prove that outer side tuples match
+ *		no more than one inner side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2133,6 +2150,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 6909359..ed70def 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -129,33 +129,29 @@ extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void initial_cost_mergejoin(PlannerInfo *root,
 					   JoinCostWorkspace *workspace,
 					   JoinType jointype,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo);
+					   JoinPathExtraData *extra);
 extern void final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo);
+					 JoinPathExtraData *extra);
 extern void initial_cost_hashjoin(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void cost_gather(GatherPath *path, PlannerInfo *root,
 			RelOptInfo *baserel, ParamPathInfo *param_info, double *rows);
 extern void cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 82d4e87..19846ac 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -119,11 +119,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -131,10 +129,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -145,11 +142,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -255,6 +250,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptKind reloptkind);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..e35d14f 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -105,6 +105,11 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   JoinType jointype,
+				   SpecialJoinInfo *sjinfo,
+				   List *restrictlist);
 
 /*
  * prototypes for plan/setrefs.c
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..9784a87 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,7 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +391,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -982,29 +983,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 564218b..a96b2a1 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -317,12 +317,11 @@ explain (costs off)
                      ->  Index Scan using ec1_expr2 on ec1 ec1_1
                      ->  Index Scan using ec1_expr3 on ec1 ec1_2
                      ->  Index Scan using ec1_expr4 on ec1 ec1_3
-               ->  Materialize
-                     ->  Sort
-                           Sort Key: ec1.f1 USING <
-                           ->  Index Scan using ec1_pkey on ec1
-                                 Index Cond: (ff = '42'::bigint)
-(20 rows)
+               ->  Sort
+                     Sort Key: ec1.f1 USING <
+                     ->  Index Scan using ec1_pkey on ec1
+                           Index Cond: (ff = '42'::bigint)
+(19 rows)
 
 -- check partially indexed scan
 set enable_nestloop = on;
@@ -374,12 +373,11 @@ explain (costs off)
                Sort Key: (((ec1_2.ff + 3) + 1))
                ->  Seq Scan on ec1 ec1_2
          ->  Index Scan using ec1_expr4 on ec1 ec1_3
-   ->  Materialize
-         ->  Sort
-               Sort Key: ec1.f1 USING <
-               ->  Index Scan using ec1_pkey on ec1
-                     Index Cond: (ff = '42'::bigint)
-(14 rows)
+   ->  Sort
+         Sort Key: ec1.f1 USING <
+         ->  Index Scan using ec1_pkey on ec1
+               Index Cond: (ff = '42'::bigint)
+(13 rows)
 
 -- check effects of row-level security
 set enable_nestloop = on;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 4992048..fe29353 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3370,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3414,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3426,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3446,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3479,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3491,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3516,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3550,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3562,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3590,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3622,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3642,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3672,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3688,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3713,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3736,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3762,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3778,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3793,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3823,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3850,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3938,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3951,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -3979,7 +4012,7 @@ select id from a where id in (
 );
          QUERY PLAN         
 ----------------------------
- Hash Semi Join
+ Hash Join
    Hash Cond: (a.id = b.id)
    ->  Seq Scan on a
    ->  Hash
@@ -4805,12 +4838,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4837,12 +4871,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4870,6 +4905,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4877,7 +4913,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4897,12 +4933,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4924,10 +4961,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4936,7 +4975,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4982,16 +5021,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5058,13 +5099,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5080,7 +5125,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5098,17 +5143,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5130,7 +5180,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5143,16 +5193,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5165,10 +5217,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5177,7 +5231,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5204,10 +5258,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5229,7 +5285,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5327,3 +5383,315 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop Left Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+                 QUERY PLAN                 
+--------------------------------------------
+ Merge Join
+   Merge Cond: (j1.id1 = j2.id1)
+   Join Filter: (j1.id2 = j2.id2)
+   ->  Index Scan using j1_id1_idx on j1
+   ->  Index Scan using j1_id1_idx on j1 j2
+(5 rows)
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+ id1 | id2 | id1 | id2 
+-----+-----+-----+-----
+   1 |   1 |   1 |   1
+   1 |   2 |   1 |   2
+(2 rows)
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 93b71c7..e1a97bc 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,13 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(8 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
@@ -5759,6 +5760,7 @@ UPDATE transition_table_base
   WHERE id BETWEEN 2 AND 3;
 INFO:  Hash Full Join
   Output: COALESCE(ot.id, nt.id), ot.val, nt.val
+  Inner Unique: No
   Hash Cond: (ot.id = nt.id)
   ->  Named Tuplestore Scan
         Output: ot.id, ot.val
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 4b6170d..65049a8 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -1991,12 +1991,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2017,11 +2018,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index ed7d6d8..063cb13 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -837,6 +837,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -845,7 +846,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -865,6 +866,7 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -882,7 +884,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index fdcc497..b16c968 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2195,6 +2195,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2202,6 +2203,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2209,6 +2211,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2216,12 +2219,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out
index d3bd8c9..7e0f3cf 100644
--- a/src/test/regress/expected/xml_1.out
+++ b/src/test/regress/expected/xml_1.out
@@ -926,12 +926,13 @@ EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1;
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- XMLNAMESPACES tests
 SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz),
@@ -1068,12 +1069,13 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- test qual
 SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) WHERE "COUNTRY_NAME" = 'Japan';
@@ -1087,13 +1089,14 @@ SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan"
 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
          Table Function Call: XMLTABLE(('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]'::text) PASSING (xmldata.data) COLUMNS "COUNTRY_NAME" text, "REGION_ID" integer)
          Filter: ("xmltable"."COUNTRY_NAME" = 'Japan'::text)
-(8 rows)
+(9 rows)
 
 -- should to work with more data
 INSERT INTO xmldata VALUES('<ROWS>
@@ -1186,13 +1189,14 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
          Filter: ("xmltable".region_id = 2)
-(8 rows)
+(9 rows)
 
 -- should fail, NULL value
 SELECT  xmltable.*
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index cca1a53..7118459 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1732,3 +1732,127 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+
+drop table j1;
+drop table j2;
+drop table j3;
#123Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#120)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

Tom, I'm wondering if you think you'll get time to look at this before the
feature freeze?

Yeah, I intend to. Thanks for updating the patch.

regards, tom lane

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

#124Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#122)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

On 2 April 2017 at 21:21, David Rowley <david.rowley@2ndquadrant.com> wrote:

I've attached an updated patch which updates the regression test output of
a recent commit to include the "Unique Inner" in the expected results.

The patch must've fallen off. Attempt number 2 at attaching.

I'm looking through this, and I'm failing to see where it deals with
the problem we discussed last time, namely that you can't apply the
optimization unless all clauses that were used in the uniqueness
proof are included in the join's merge/hash conditions + joinquals.

It might be sufficient to ignore is_pushed_down conditions (at an
outer join only) while trying to make the proofs; but if it's doing
that, I don't see where.

I don't especially like the centralized unique_rels cache structure.
It's not terribly clear what it's for, and you're making uncomfortably
large assumptions about never indexing off the end of the array, despite
not having any range checks for the subscripts. Wouldn't it be better to
add simple List fields into RelOptInfo, representing the outer rels this
rel has been proven unique or not-unique for? That would dodge the array
size question and would be more readily extensible to someday applying
this to join rels.

I also think some more thought is needed about handling JOIN_UNIQUE_OUTER
and JOIN_UNIQUE_INNER cases. In the first place, the patch cavalierly
violates the statement in joinpath.c that those jointype values never
propagate outside that module. In the second place, shouldn't
JOIN_UNIQUE_INNER automatically result in the path getting marked
inner_unique? I suspect the logic in add_paths_to_joinrel ought to
look something like

if (jointype == JOIN_UNIQUE_INNER)
extra.inner_unique = true;
else
extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
(jointype == JOIN_UNIQUE_OUTER ? JOIN_INNER : jointype),
restrictlist);

regards, tom lane

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

#125David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#124)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 7 April 2017 at 07:26, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I'm looking through this, and I'm failing to see where it deals with
the problem we discussed last time, namely that you can't apply the
optimization unless all clauses that were used in the uniqueness
proof are included in the join's merge/hash conditions + joinquals.

Many thanks for looking at this again.

The test in question is in nodeMergeJoin.c. I believe the join is
still unique no matter where the clauses are evaluated. It should be
up to the executor code to make use of the knowledge how it sees fit.
The planner should not make assumptions on how the executor will make
use of this knowledge. I've carefully crafted a comment in
nodeMergejoin.c which explains all of this, and also about the
limitations and where things might be improved later.

The code in question is:

mergestate->mj_SkipMarkRestore = !mergestate->js.joinqual &&
mergestate->js.first_inner_tuple_only;

I don't especially like the centralized unique_rels cache structure.
It's not terribly clear what it's for, and you're making uncomfortably
large assumptions about never indexing off the end of the array, despite
not having any range checks for the subscripts. Wouldn't it be better to
add simple List fields into RelOptInfo, representing the outer rels this
rel has been proven unique or not-unique for? That would dodge the array
size question and would be more readily extensible to someday applying
this to join rels .

hmm, perhaps bounds checking could be done, but it's no worse than
planner_rt_fetch().
I don't really think the List idea would be nearly as efficient. The
array provides a direct lookup for the List of proof relations. A List
of List list would require a linear lookup just to find the correct
List, then the existing linear lookup to find the proofs. The cache is
designed to be fast. Slowing it down seems like a bad idea. Perhaps
throwing it away would be better, since it's not required and was only
added as an optimisation.

The non_unique_rels will most often have a NULL bitmap set due to the
incremental join search by the standard planner. So access to this as
an array should be very fast, as we'll quickly realise there are no
proofs to be found.

I also think some more thought is needed about handling JOIN_UNIQUE_OUTER
and JOIN_UNIQUE_INNER cases. In the first place, the patch cavalierly
violates the statement in joinpath.c that those jointype values never
propagate outside that module. In the second place, shouldn't
JOIN_UNIQUE_INNER automatically result in the path getting marked
inner_unique? I suspect the logic in add_paths_to_joinrel ought to
look something like

if (jointype == JOIN_UNIQUE_INNER)
extra.inner_unique = true;
else
extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
(jointype == JOIN_UNIQUE_OUTER ? JOIN_INNER : jointype),
restrictlist);

hmm, innerrel_is_unique() is without prejudice as to the jointypes it
supports, so much so that the argument is completely ignored. It was
probably left over from some previous incarnation of the patch.
If we treat JOIN_UNIQUE_INNER specially, then we'd better be sure that
it's made unique on the RHS join quals. It looks like
create_unique_path() uses sjinfo->semi_rhs_exprs to uniquify the
relation, and compute_semijoin_info() seems to take all of the join
conditions there or nothing at all, so I think it's safe to
automatically mark JOIN_UNIQUE_INNERs this way.

Updated patch is attached.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-04-07.patchapplication/octet-stream; name=unique_joins_2017-04-07.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a18ab43..009d982 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1343,6 +1343,25 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+				{
+					const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+
+					ExplainPropertyText("Inner Unique", value, es);
+					break;
+				}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index f2c885a..b73cb90 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -288,10 +288,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NULL || ExecQual(otherqual, econtext))
@@ -399,6 +399,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -438,8 +446,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			hjstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 62784af..4a2a38d 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -802,10 +802,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NULL ||
@@ -826,7 +826,18 @@ ExecMergeJoin(MergeJoinState *node)
 						InstrCountFiltered2(node, 1);
 				}
 				else
+				{
+					/*
+					 * SkipMarkRestore will be set if we only require a single
+					 * match on the inner side, or if there can only be one
+					 * match. We can safely skip to the next outer tuple in
+					 * this case.
+					 */
+					if (node->mj_SkipMarkRestore)
+						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
+
 					InstrCountFiltered1(node, 1);
+				}
 				break;
 
 				/*
@@ -1048,7 +1059,9 @@ ExecMergeJoin(MergeJoinState *node)
 					/*
 					 * the merge clause matched so now we restore the inner
 					 * scan position to the first mark, and go join that tuple
-					 * (and any following ones) to the new outer.
+					 * (and any following ones) to the new outer.  If we were
+					 * able to determine mark and restore would be a no-op,
+					 * then we can skip this rigmarole.
 					 *
 					 * NOTE: we do not need to worry about the MatchedInner
 					 * state for the rescanned inner tuples.  We know all of
@@ -1062,17 +1075,20 @@ ExecMergeJoin(MergeJoinState *node)
 					 * forcing the merge clause to never match, so we never
 					 * get here.
 					 */
-					ExecRestrPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecRestrPos(innerPlan);
 
-					/*
-					 * ExecRestrPos probably should give us back a new Slot,
-					 * but since it doesn't, use the marked slot.  (The
-					 * previously returned mj_InnerTupleSlot cannot be assumed
-					 * to hold the required tuple.)
-					 */
-					node->mj_InnerTupleSlot = innerTupleSlot;
-					/* we need not do MJEvalInnerValues again */
+						/*
+						 * ExecRestrPos probably should give us back a new
+						 * Slot, but since it doesn't, use the marked slot.
+						 * (The previously returned mj_InnerTupleSlot cannot
+						 * be assumed to hold the required tuple.)
+						 */
+						node->mj_InnerTupleSlot = innerTupleSlot;
+					}
 
+					/* we need not do MJEvalInnerValues again */
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
@@ -1172,9 +1188,12 @@ ExecMergeJoin(MergeJoinState *node)
 
 				if (compareResult == 0)
 				{
-					ExecMarkPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecMarkPos(innerPlan);
 
-					MarkInnerTuple(node->mj_InnerTupleSlot, node);
+						MarkInnerTuple(node->mj_InnerTupleSlot, node);
+					}
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
@@ -1438,6 +1457,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1499,8 +1526,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			mergestate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
@@ -1581,6 +1614,36 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->mj_InnerTupleSlot = NULL;
 
 	/*
+	 * When matching to the first inner tuple only, as in the case with unique
+	 * and SEMI joins, if the joinqual list is empty (i.e all join conditions
+	 * are present in as merge join clauses), then when we find an inner tuple
+	 * matching the outer tuple, we've no need to keep scanning the inner side
+	 * looking for more tuples matching this outer tuple.  This means we're
+	 * never going to advance the inner side to check for more tuples which
+	 * match the current outer side tuple.  This basically boil down to, any
+	 * mark/restore operations we would perform would be a no-op.  The inner
+	 * side will already be in the correct location.  Here we detect this and
+	 * record that no mark and restore is required during the join.
+	 *
+	 * This mark and restore skipping can provide a good performance boost,
+	 * and it is a fairly common case, so it's certainly worth the cost of the
+	 * additional checks which must be performed during the join.
+	 *
+	 * We could apply this optimization in more casee for unique joins. We'd
+	 * just need to ensure that all of the quals which were used to prove the
+	 * inner side is unique are present in the merge join clauses. Currently
+	 * we've no way to get this information from the planner, but it may be
+	 * worth some exploration for ways to improve this in the future. Here we
+	 * can assume all the unique quals are present as merge join quals by the
+	 * lack of any joinquals.  Further tuning in this area is not possible for
+	 * SEMI joins.
+	 *
+	 * XXX if changing this see logic in final_cost_mergejoin()
+	 */
+	mergestate->mj_SkipMarkRestore = !mergestate->js.joinqual &&
+		mergestate->js.first_inner_tuple_only;
+
+	/*
 	 * initialization successful
 	 */
 	MJ1_printf("ExecInitMergeJoin: %s\n",
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 53977e0..bd16af1 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -219,10 +219,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need to join to the
+			 * first inner matching tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.first_inner_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NULL || ExecQual(otherqual, econtext))
@@ -273,6 +273,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -311,8 +319,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			nlstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index bf0fb56..12bdcd7 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2081,15 +2081,13 @@ cost_group(Path *path, PlannerInfo *root,
  * 'jointype' is the type of join to be performed
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2120,10 +2118,13 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -2156,17 +2157,16 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  *
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->outerjoinpath;
 	Path	   *inner_path = path->innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	Cost		startup_cost = workspace->startup_cost;
@@ -2206,10 +2206,13 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2350,7 +2353,7 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
  * 'inner_path' is the inner input to the join
  * 'outersortkeys' is the list of sort keys for the outer path
  * 'innersortkeys' is the list of sort keys for the inner path
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  *
  * Note: outersortkeys and innersortkeys should be NIL if no explicit
  * sort is needed because the respective source path is already ordered.
@@ -2361,7 +2364,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo)
+					   JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2438,7 +2441,8 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerstartsel = cache->leftstartsel;
 			innerendsel = cache->leftendsel;
 		}
-		if (jointype == JOIN_LEFT ||
+		if (extra->inner_unique ||
+			jointype == JOIN_LEFT ||
 			jointype == JOIN_ANTI)
 		{
 			outerstartsel = 0.0;
@@ -2576,12 +2580,12 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		materialize_inner
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo)
+					 JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
@@ -2670,9 +2674,14 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * computations?
 	 *
 	 * The whole issue is moot if we are working from a unique-ified outer
-	 * input.
+	 * input. We'll also never rescan during SEMI/ANTI/inner_unique as there's
+	 * no need to advance the inner side to find subsequent matches, therefore
+	 * no need to mark/restore.
 	 */
-	if (IsA(outer_path, UniquePath))
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		IsA(outer_path, UniquePath))
 		rescannedtuples = 0;
 	else
 	{
@@ -2721,7 +2730,11 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	/*
 	 * Even if materializing doesn't look cheaper, we *must* do it if the
 	 * inner path is to be used directly (without sorting) and it doesn't
-	 * support mark/restore.
+	 * support mark/restore. However, if the inner side of the join is unique
+	 * or we're performing a SEMI join, and all of the joinrestrictinfos are
+	 * present as merge clauses, then we may be able to skip mark and restore
+	 * completely.  The logic here must follow the logic that is used to set
+	 * mj_SkipMarkRestore in ExecInitMergeJoin().
 	 *
 	 * Since the inner side must be ordered, and only Sorts and IndexScans can
 	 * create order to begin with, and they both support mark/restore, you
@@ -2735,6 +2748,9 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * it off does not entitle us to deliver an invalid plan.
 	 */
 	else if (innersortkeys == NIL &&
+			 !((extra->inner_unique || path->jpath.jointype == JOIN_SEMI) &&
+			   list_length(path->jpath.joinrestrictinfo) ==
+			   list_length(path->path_mergeclauses)) &&
 			 !ExecSupportsMarkRestore(inner_path))
 		path->materialize_inner = true;
 
@@ -2876,16 +2892,14 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
  * 'hashclauses' is the list of joinclauses to be used as hash clauses
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2970,17 +2984,16 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	List	   *hashclauses = path->path_hashclauses;
@@ -3101,13 +3114,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index de7044d..9fd7b75 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -21,6 +21,7 @@
 #include "optimizer/cost.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/planmain.h"
 
 /* Hook for plugins to get control in add_paths_to_joinrel() */
 set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
@@ -121,6 +122,17 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.  JOIN_UNIQUE_INNER gets a free pass
+	 * because it'll be made unique on the join condition later.
+	 */
+	if (jointype == JOIN_UNIQUE_INNER)
+		extra.inner_unique = true;
+	else
+		extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+												restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -336,8 +348,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * methodology worthwhile.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -348,11 +359,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -399,8 +408,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
 
@@ -410,11 +418,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -486,7 +492,7 @@ try_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -497,10 +503,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -562,7 +567,7 @@ try_partial_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
@@ -573,10 +578,9 @@ try_partial_mergejoin_path(PlannerInfo *root,
 										   joinrel,
 										   jointype,
 										   &workspace,
-										   extra->sjinfo,
+										   extra,
 										   outer_path,
 										   inner_path,
-										   extra->restrictlist,
 										   pathkeys,
 										   NULL,
 										   mergeclauses,
@@ -620,8 +624,7 @@ try_hashjoin_path(PlannerInfo *root,
 	 * never have any output pathkeys, per comments in create_hashjoin_path.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -632,11 +635,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -683,8 +684,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, NIL))
 		return;
 
@@ -694,11 +694,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index ac63f75..ace01a0 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -41,6 +41,10 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
 
 
 /*
@@ -845,3 +849,160 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' is unique based on the join
+ *	  condition in 'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   List *restrictlist)
+{
+	MemoryContext	old_context;
+	ListCell	   *lc;
+	int				innerrelid;
+	List		   *cachelist;
+
+	/* can't prove uniqueness for joins with an empty restrictlist */
+	if (restrictlist == NIL)
+		return false;
+
+	/*
+	 * Currently we're unable to prove uniqueness when there's more than one
+	 * relation on the inner side of the join.
+	 */
+	if (!bms_get_singleton_member(innerrel->relids, &innerrelid))
+		return false;
+
+	/*
+	 * First let's query the unique and non-unique caches to see if we've
+	 * managed to prove that innerrel is unique for some subset of this
+	 * outerrel. We don't need an exact match, as if we have any extra
+	 * outerrels than were previously cached, then they can't make the
+	 * innerrel any less unique.
+	 */
+	cachelist = fetch_unique_rel_list(root, innerrelid);
+	foreach(lc, cachelist)
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(unique_rels, outerrel->relids))
+			return true;		/* Success! */
+	}
+
+	/*
+	 * We may have previously determined that this outerrel, or some superset
+	 * thereof, cannot prove this innerrel to be unique.
+	 */
+	cachelist = fetch_non_unique_rel_list(root, innerrelid);
+	foreach(lc, cachelist)
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(outerrel->relids, unique_rels))
+			return false;
+	}
+
+	/* No cached information, so try to make the proof. */
+	if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+	{
+		/*
+		 * Cache the positive result for future probes, being sure to keep it
+		 * in the planner_cxt even if we are working in GEQO.
+		 *
+		 * Note: one might consider trying to isolate the minimal subset of
+		 * the outerrels that proved the innerrel unique.  But it's not worth
+		 * the trouble, because the planner builds up joinrels incrementally
+		 * and so we'll see the minimally sufficient outerrels before any
+		 * supersets of them anyway.
+		 */
+		old_context = MemoryContextSwitchTo(root->planner_cxt);
+		root->unique_rels[innerrelid] =
+			lappend(root->unique_rels[innerrelid],
+					bms_copy(outerrel->relids));
+		MemoryContextSwitchTo(old_context);
+
+		return true;		/* Success! */
+	}
+	else
+	{
+		/*
+		 * None of the join conditions for outerrel proved innerrel unique, so
+		 * we can safely reject this outerrel or any subset of it in future
+		 * checks.
+		 *
+		 * However, in normal planning mode, caching this knowledge is totally
+		 * pointless; it won't be queried again, because we build up joinrels
+		 * from smaller to larger.  It is useful in GEQO mode, where the
+		 * knowledge can be carried across successive planning attempts; and
+		 * it's likely to be useful when using join-search plugins, too.
+		 * Hence cache only when join_search_private is non-NULL.  (Yeah,
+		 * that's a hack, but it seems reasonable.)
+		 */
+		if (root->join_search_private)
+		{
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			root->non_unique_rels[innerrelid] =
+				lappend(root->non_unique_rels[innerrelid],
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+
+		return false;
+	}
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index b121f40..a327b48 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -215,12 +215,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -235,7 +235,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3714,7 +3714,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -4016,7 +4016,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -4156,7 +4156,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5349,7 +5349,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5358,9 +5358,10 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5372,7 +5373,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5382,8 +5383,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5424,7 +5426,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5438,8 +5440,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index ef0de3f..26c339f 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -124,6 +124,9 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	setup_simple_rel_arrays(root);
 
+	/* Allocate memory for caching which joins are unique. */
+	setup_unique_join_caches(root);
+
 	/*
 	 * Construct RelOptInfo nodes for all base relations in query, and
 	 * indirectly for all appendrel member relations ("other rels").  This
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 8536212..a2875dc 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -2049,11 +2049,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -2064,16 +2062,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -2109,7 +2106,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2122,8 +2119,9 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2136,10 +2134,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2152,10 +2149,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2163,6 +2159,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2172,7 +2169,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2185,12 +2182,13 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2202,11 +2200,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2216,15 +2212,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2234,7 +2229,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2259,10 +2254,11 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 3aa701f..98f54ad 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -84,6 +84,22 @@ setup_simple_rel_arrays(PlannerInfo *root)
 }
 
 /*
+ * setup_unique_join_caches
+ *	  Prepare the arrays we use for caching which joins are proved to be
+ *	  unique and non-unique.
+ */
+void
+setup_unique_join_caches(PlannerInfo *root)
+{
+	int			size = root->simple_rel_array_size;
+
+	/* initialize the unique relation cache to all NULLs */
+	root->unique_rels = (List **) palloc0(size * sizeof(List *));
+
+	root->non_unique_rels = (List **) palloc0(size * sizeof(List *));
+}
+
+/*
  * build_simple_rel
  *	  Construct a new RelOptInfo for a base relation or 'other' relation.
  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index fa99244..66ed681 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1537,6 +1537,9 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	ExprState  *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		first_inner_tuple_only; /* True if we should stop searching
+										 * for more inner tuples after finding
+										 * the first match */
 } JoinState;
 
 /* ----------------
@@ -1567,6 +1570,7 @@ typedef struct NestLoopState
  *		FillInner		   true if should emit unjoined inner tuples anyway
  *		MatchedOuter	   true if found a join match for current outer tuple
  *		MatchedInner	   true if found a join match for current inner tuple
+ *		SkipMarkRestore    true if mark and restore are certainly no-ops
  *		OuterTupleSlot	   slot in tuple table for cur outer tuple
  *		InnerTupleSlot	   slot in tuple table for cur inner tuple
  *		MarkedTupleSlot    slot in tuple table for marked tuple
@@ -1591,6 +1595,7 @@ typedef struct MergeJoinState
 	bool		mj_FillInner;
 	bool		mj_MatchedOuter;
 	bool		mj_MatchedInner;
+	bool		mj_SkipMarkRestore;
 	TupleTableSlot *mj_OuterTupleSlot;
 	TupleTableSlot *mj_InnerTupleSlot;
 	TupleTableSlot *mj_MarkedTupleSlot;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index a2dd26f..b16d04c 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -634,6 +634,8 @@ typedef struct CustomScan
  * jointype:	rule for joining tuples from left and right subtrees
  * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
  *				(plan.qual contains conditions that came from WHERE)
+ * inner_unique each outer tuple can match to no more than one inner
+ *				tuple
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -649,6 +651,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 6bad18e..062570e 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -224,6 +224,18 @@ typedef struct PlannerInfo
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
 	int			join_cur_level; /* index of list being extended */
 
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner side based on the join condition.
+	 * This is a rather expensive test to perform, and requires looking for
+	 * proofs such as a relation's unique indexes.  We use this cache during
+	 * the join search to record lists of the sets of relations which both
+	 * prove, and disprove the uniqueness properties for the relid indexed by
+	 * these arrays.
+	 */
+	List	  **unique_rels;	/* cache for proven unique rels */
+	List	  **non_unique_rels;	/* cache for proven non-unique rels */
+
 	List	   *init_plans;		/* init SubPlans for query */
 
 	List	   *cte_plan_ids;	/* per-CTE-item list of subplan IDs */
@@ -325,6 +337,15 @@ typedef struct PlannerInfo
 	((root)->simple_rte_array ? (root)->simple_rte_array[rti] : \
 	 rt_fetch(rti, (root)->parse->rtable))
 
+/* Fetch the correct unique relation cache entry for relid */
+#define fetch_unique_rel_list(root, relid) \
+	((relid) >= (root)->simple_rel_array_size ? NIL : \
+	(root)->unique_rels[relid])
+
+/* Fetch the correct non-unique relation cache entry for relid */
+#define fetch_non_unique_rel_list(root, relid) \
+	((relid) >= (root)->simple_rel_array_size ? NIL : \
+	(root)->non_unique_rels[relid])
 
 /*----------
  * RelOptInfo
@@ -1277,6 +1298,9 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* each outer tuple can match to no more than
+								 * one inner tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -2143,6 +2167,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique true if proofs exist which prove that outer side tuples match
+ *		no more than one inner side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2151,6 +2177,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 81a84b5..7b67a05 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -129,33 +129,29 @@ extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void initial_cost_mergejoin(PlannerInfo *root,
 					   JoinCostWorkspace *workspace,
 					   JoinType jointype,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo);
+					   JoinPathExtraData *extra);
 extern void final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo);
+					 JoinPathExtraData *extra);
 extern void initial_cost_hashjoin(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void cost_gather(GatherPath *path, PlannerInfo *root,
 			RelOptInfo *baserel, ParamPathInfo *param_info, double *rows);
 extern void cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 2e712c6..25182f4 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -119,11 +119,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -131,10 +129,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -145,11 +142,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -255,6 +250,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptInfo *parent);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..7ea03ca 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -105,6 +105,9 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   List *restrictlist);
 
 /*
  * prototypes for plan/setrefs.c
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..9784a87 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,7 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +391,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -982,29 +983,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 564218b..a96b2a1 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -317,12 +317,11 @@ explain (costs off)
                      ->  Index Scan using ec1_expr2 on ec1 ec1_1
                      ->  Index Scan using ec1_expr3 on ec1 ec1_2
                      ->  Index Scan using ec1_expr4 on ec1 ec1_3
-               ->  Materialize
-                     ->  Sort
-                           Sort Key: ec1.f1 USING <
-                           ->  Index Scan using ec1_pkey on ec1
-                                 Index Cond: (ff = '42'::bigint)
-(20 rows)
+               ->  Sort
+                     Sort Key: ec1.f1 USING <
+                     ->  Index Scan using ec1_pkey on ec1
+                           Index Cond: (ff = '42'::bigint)
+(19 rows)
 
 -- check partially indexed scan
 set enable_nestloop = on;
@@ -374,12 +373,11 @@ explain (costs off)
                Sort Key: (((ec1_2.ff + 3) + 1))
                ->  Seq Scan on ec1 ec1_2
          ->  Index Scan using ec1_expr4 on ec1 ec1_3
-   ->  Materialize
-         ->  Sort
-               Sort Key: ec1.f1 USING <
-               ->  Index Scan using ec1_pkey on ec1
-                     Index Cond: (ff = '42'::bigint)
-(14 rows)
+   ->  Sort
+         Sort Key: ec1.f1 USING <
+         ->  Index Scan using ec1_pkey on ec1
+               Index Cond: (ff = '42'::bigint)
+(13 rows)
 
 -- check effects of row-level security
 set enable_nestloop = on;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 4992048..fe29353 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3370,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3414,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3426,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3446,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3479,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3491,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3516,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3550,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3562,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3590,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3622,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3642,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3672,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3688,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3713,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3736,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3762,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3778,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3793,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3823,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3850,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3938,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3951,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -3979,7 +4012,7 @@ select id from a where id in (
 );
          QUERY PLAN         
 ----------------------------
- Hash Semi Join
+ Hash Join
    Hash Cond: (a.id = b.id)
    ->  Seq Scan on a
    ->  Hash
@@ -4805,12 +4838,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4837,12 +4871,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4870,6 +4905,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4877,7 +4913,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4897,12 +4933,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4924,10 +4961,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4936,7 +4975,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4982,16 +5021,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5058,13 +5099,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5080,7 +5125,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5098,17 +5143,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5130,7 +5180,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5143,16 +5193,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5165,10 +5217,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5177,7 +5231,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5204,10 +5258,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5229,7 +5285,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5327,3 +5383,315 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop Left Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+                 QUERY PLAN                 
+--------------------------------------------
+ Merge Join
+   Merge Cond: (j1.id1 = j2.id1)
+   Join Filter: (j1.id2 = j2.id2)
+   ->  Index Scan using j1_id1_idx on j1
+   ->  Index Scan using j1_id1_idx on j1 j2
+(5 rows)
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+ id1 | id2 | id1 | id2 
+-----+-----+-----+-----
+   1 |   1 |   1 |   1
+   1 |   2 |   1 |   2
+(2 rows)
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 2beb658..c6bf803 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,13 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(8 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
@@ -5759,6 +5760,7 @@ UPDATE transition_table_base
   WHERE id BETWEEN 2 AND 3;
 INFO:  Hash Full Join
   Output: COALESCE(ot.id, nt.id), ot.val, nt.val
+  Inner Unique: No
   Hash Cond: (ot.id = nt.id)
   ->  Named Tuplestore Scan
         Output: ot.id, ot.val
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 4b6170d..65049a8 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -1991,12 +1991,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2017,11 +2018,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index ed7d6d8..063cb13 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -837,6 +837,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -845,7 +846,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -865,6 +866,7 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -882,7 +884,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index fdcc497..b16c968 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2195,6 +2195,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2202,6 +2203,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2209,6 +2211,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2216,12 +2219,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out
index d3bd8c9..7e0f3cf 100644
--- a/src/test/regress/expected/xml_1.out
+++ b/src/test/regress/expected/xml_1.out
@@ -926,12 +926,13 @@ EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1;
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- XMLNAMESPACES tests
 SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz),
@@ -1068,12 +1069,13 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- test qual
 SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) WHERE "COUNTRY_NAME" = 'Japan';
@@ -1087,13 +1089,14 @@ SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan"
 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
          Table Function Call: XMLTABLE(('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]'::text) PASSING (xmldata.data) COLUMNS "COUNTRY_NAME" text, "REGION_ID" integer)
          Filter: ("xmltable"."COUNTRY_NAME" = 'Japan'::text)
-(8 rows)
+(9 rows)
 
 -- should to work with more data
 INSERT INTO xmldata VALUES('<ROWS>
@@ -1186,13 +1189,14 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
          Filter: ("xmltable".region_id = 2)
-(8 rows)
+(9 rows)
 
 -- should fail, NULL value
 SELECT  xmltable.*
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index cca1a53..7118459 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1732,3 +1732,127 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+
+drop table j1;
+drop table j2;
+drop table j3;
#126Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#125)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

On 7 April 2017 at 07:26, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I'm looking through this, and I'm failing to see where it deals with
the problem we discussed last time, namely that you can't apply the
optimization unless all clauses that were used in the uniqueness
proof are included in the join's merge/hash conditions + joinquals.

The code in question is:
mergestate->mj_SkipMarkRestore = !mergestate->js.joinqual &&
mergestate->js.first_inner_tuple_only;

Uh, AFAICS that only protects the skip-mark-and-restore logic.
What I'm on about is that you can't do the early advance to the
next outer tuple unless you're sure that all the quals that were
relevant to the uniqueness proof have been checked for the current
inner tuple. That affects all three join types not only merge.

The case that would be relevant to this is, eg,

create table t1 (f1 int, f2 int, primary key(f1,f2));

select * from t_outer left join t1 on (t_outer.f1 = t1.f1)
where t_outer.f2 = t2.f2;

Your existing patch would think t1 is unique-inner, but the qual pushed
down from WHERE would not be a joinqual so the wrong thing would happen
at runtime.

(Hm ... actually, this example wouldn't fail as written because
the WHERE qual is probably strict, so the left join would get
reduced to an inner join and then pushed-down-ness no longer
matters. But hopefully you get my drift.)

I don't really think the List idea would be nearly as efficient.

No, what I'm saying is that each RelOptInfo would contain a single List of
Relids of proven-unique-for outer rels (and another one for the negative
cache). No array, no more searching than you have now, just removal of an
uncertainly-safe array fetch.

regards, tom lane

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

#127David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#126)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 7 April 2017 at 11:47, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

On 7 April 2017 at 07:26, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I'm looking through this, and I'm failing to see where it deals with
the problem we discussed last time, namely that you can't apply the
optimization unless all clauses that were used in the uniqueness
proof are included in the join's merge/hash conditions + joinquals.

The code in question is:
mergestate->mj_SkipMarkRestore = !mergestate->js.joinqual &&
mergestate->js.first_inner_tuple_only;

Uh, AFAICS that only protects the skip-mark-and-restore logic.
What I'm on about is that you can't do the early advance to the
next outer tuple unless you're sure that all the quals that were
relevant to the uniqueness proof have been checked for the current
inner tuple. That affects all three join types not only merge.

Well, look at the join code and you'll see this only happens after the
joinqual is evaulated. I didn't make a special effort here. I just
borrowed the location that JOIN_SEMI was already using.

For example, from hash join:

if (joinqual == NULL || ExecQual(joinqual, econtext))
{
node->hj_MatchedOuter = true;
HeapTupleHeaderSetMatch(HJTUPLE_MINTUPLE(node->hj_CurTuple));

/* In an antijoin, we never return a matched tuple */
if (node->js.jointype == JOIN_ANTI)
{
node->hj_JoinState = HJ_NEED_NEW_OUTER;
continue;
}

/*
* Skip to the next outer tuple if we only need to join to
* the first inner matching tuple.
*/
if (node->js.first_inner_tuple_only)
node->hj_JoinState = HJ_NEED_NEW_OUTER;

Note the first line and the final two lines.

Here's the one from nested loop:

if (ExecQual(joinqual, econtext))
{
node->nl_MatchedOuter = true;

/* In an antijoin, we never return a matched tuple */
if (node->js.jointype == JOIN_ANTI)
{
node->nl_NeedNewOuter = true;
continue; /* return to top of loop */
}

/*
* Skip to the next outer tuple if we only need to join to the
* first inner matching tuple.
*/
if (node->js.first_inner_tuple_only)
node->nl_NeedNewOuter = true;

Again, note the first line and final 2 lines.

The case that would be relevant to this is, eg,

create table t1 (f1 int, f2 int, primary key(f1,f2));

select * from t_outer left join t1 on (t_outer.f1 = t1.f1)
where t_outer.f2 = t2.f2;

hmm, that query is not valid, unless you have created some table named
t_outer. I don't know how you've defined that. So I guess you must
have meant:

postgres=# explain verbose select * from t1 t_outer left join t1 on
(t_outer.f1 = t1.f1) where t_outer.f2 = t1.f2;
QUERY PLAN
---------------------------------------------------------------------------
Hash Join (cost=66.50..133.57 rows=128 width=16)
Output: t_outer.f1, t_outer.f2, t1.f1, t1.f2
Inner Unique: Yes
Hash Cond: ((t_outer.f1 = t1.f1) AND (t_outer.f2 = t1.f2))
-> Seq Scan on public.t1 t_outer (cost=0.00..32.60 rows=2260 width=8)
Output: t_outer.f1, t_outer.f2
-> Hash (cost=32.60..32.60 rows=2260 width=8)
Output: t1.f1, t1.f2
-> Seq Scan on public.t1 (cost=0.00..32.60 rows=2260 width=8)
Output: t1.f1, t1.f2
(10 rows)

Which did become an INNER JOIN due to the strict W

If you'd had done:

postgres=# explain verbose select * from t1 t_outer left join t1 on
(t_outer.f1 = t1.f1) where t_outer.f2 = t1.f2 or t1.f1 is null;
QUERY PLAN
------------------------------------------------------------------------------------------------
Merge Left Join (cost=0.31..608.67 rows=255 width=16)
Output: t_outer.f1, t_outer.f2, t1.f1, t1.f2
Inner Unique: No
Merge Cond: (t_outer.f1 = t1.f1)
Filter: ((t_outer.f2 = t1.f2) OR (t1.f1 IS NULL))
-> Index Only Scan using t1_pkey on public.t1 t_outer
(cost=0.16..78.06 rows=2260 width=8)
Output: t_outer.f1, t_outer.f2
-> Materialize (cost=0.16..83.71 rows=2260 width=8)
Output: t1.f1, t1.f2
-> Index Only Scan using t1_pkey on public.t1
(cost=0.16..78.06 rows=2260 width=8)
Output: t1.f1, t1.f2
(11 rows)

You'll notice that "Inner Unique: No"

Your existing patch would think t1 is unique-inner, but the qual pushed
down from WHERE would not be a joinqual so the wrong thing would happen
at runtime.

(Hm ... actually, this example wouldn't fail as written because
the WHERE qual is probably strict, so the left join would get
reduced to an inner join and then pushed-down-ness no longer
matters. But hopefully you get my drift.)

Oh yeah. I get it, but that's why we ignore !can_join clauses

/* Ignore if it's not a mergejoinable clause */
if (!restrictinfo->can_join ||
restrictinfo->mergeopfamilies == NIL)
continue; /* not mergejoinable */

no?

I don't really think the List idea would be nearly as efficient.

No, what I'm saying is that each RelOptInfo would contain a single List of
Relids of proven-unique-for outer rels (and another one for the negative
cache). No array, no more searching than you have now, just removal of an
uncertainly-safe array fetch.

That's way cleaner. Thanks. I've changed it that way. Feeling silly
now for having done it the original way.

Updated patch is attached.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-04-07a.patchapplication/octet-stream; name=unique_joins_2017-04-07a.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a18ab43..009d982 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1343,6 +1343,25 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+				{
+					const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+
+					ExplainPropertyText("Inner Unique", value, es);
+					break;
+				}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index f2c885a..b73cb90 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -288,10 +288,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NULL || ExecQual(otherqual, econtext))
@@ -399,6 +399,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -438,8 +446,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			hjstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 62784af..4a2a38d 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -802,10 +802,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NULL ||
@@ -826,7 +826,18 @@ ExecMergeJoin(MergeJoinState *node)
 						InstrCountFiltered2(node, 1);
 				}
 				else
+				{
+					/*
+					 * SkipMarkRestore will be set if we only require a single
+					 * match on the inner side, or if there can only be one
+					 * match. We can safely skip to the next outer tuple in
+					 * this case.
+					 */
+					if (node->mj_SkipMarkRestore)
+						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
+
 					InstrCountFiltered1(node, 1);
+				}
 				break;
 
 				/*
@@ -1048,7 +1059,9 @@ ExecMergeJoin(MergeJoinState *node)
 					/*
 					 * the merge clause matched so now we restore the inner
 					 * scan position to the first mark, and go join that tuple
-					 * (and any following ones) to the new outer.
+					 * (and any following ones) to the new outer.  If we were
+					 * able to determine mark and restore would be a no-op,
+					 * then we can skip this rigmarole.
 					 *
 					 * NOTE: we do not need to worry about the MatchedInner
 					 * state for the rescanned inner tuples.  We know all of
@@ -1062,17 +1075,20 @@ ExecMergeJoin(MergeJoinState *node)
 					 * forcing the merge clause to never match, so we never
 					 * get here.
 					 */
-					ExecRestrPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecRestrPos(innerPlan);
 
-					/*
-					 * ExecRestrPos probably should give us back a new Slot,
-					 * but since it doesn't, use the marked slot.  (The
-					 * previously returned mj_InnerTupleSlot cannot be assumed
-					 * to hold the required tuple.)
-					 */
-					node->mj_InnerTupleSlot = innerTupleSlot;
-					/* we need not do MJEvalInnerValues again */
+						/*
+						 * ExecRestrPos probably should give us back a new
+						 * Slot, but since it doesn't, use the marked slot.
+						 * (The previously returned mj_InnerTupleSlot cannot
+						 * be assumed to hold the required tuple.)
+						 */
+						node->mj_InnerTupleSlot = innerTupleSlot;
+					}
 
+					/* we need not do MJEvalInnerValues again */
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
@@ -1172,9 +1188,12 @@ ExecMergeJoin(MergeJoinState *node)
 
 				if (compareResult == 0)
 				{
-					ExecMarkPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecMarkPos(innerPlan);
 
-					MarkInnerTuple(node->mj_InnerTupleSlot, node);
+						MarkInnerTuple(node->mj_InnerTupleSlot, node);
+					}
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
@@ -1438,6 +1457,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1499,8 +1526,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			mergestate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
@@ -1581,6 +1614,36 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->mj_InnerTupleSlot = NULL;
 
 	/*
+	 * When matching to the first inner tuple only, as in the case with unique
+	 * and SEMI joins, if the joinqual list is empty (i.e all join conditions
+	 * are present in as merge join clauses), then when we find an inner tuple
+	 * matching the outer tuple, we've no need to keep scanning the inner side
+	 * looking for more tuples matching this outer tuple.  This means we're
+	 * never going to advance the inner side to check for more tuples which
+	 * match the current outer side tuple.  This basically boil down to, any
+	 * mark/restore operations we would perform would be a no-op.  The inner
+	 * side will already be in the correct location.  Here we detect this and
+	 * record that no mark and restore is required during the join.
+	 *
+	 * This mark and restore skipping can provide a good performance boost,
+	 * and it is a fairly common case, so it's certainly worth the cost of the
+	 * additional checks which must be performed during the join.
+	 *
+	 * We could apply this optimization in more casee for unique joins. We'd
+	 * just need to ensure that all of the quals which were used to prove the
+	 * inner side is unique are present in the merge join clauses. Currently
+	 * we've no way to get this information from the planner, but it may be
+	 * worth some exploration for ways to improve this in the future. Here we
+	 * can assume all the unique quals are present as merge join quals by the
+	 * lack of any joinquals.  Further tuning in this area is not possible for
+	 * SEMI joins.
+	 *
+	 * XXX if changing this see logic in final_cost_mergejoin()
+	 */
+	mergestate->mj_SkipMarkRestore = !mergestate->js.joinqual &&
+		mergestate->js.first_inner_tuple_only;
+
+	/*
 	 * initialization successful
 	 */
 	MJ1_printf("ExecInitMergeJoin: %s\n",
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 53977e0..bd16af1 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -219,10 +219,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need to join to the
+			 * first inner matching tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.first_inner_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NULL || ExecQual(otherqual, econtext))
@@ -273,6 +273,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -311,8 +319,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			nlstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 1367297..c8bf3a4 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2251,6 +2251,8 @@ _outRelOptInfo(StringInfo str, const RelOptInfo *node)
 	WRITE_NODE_FIELD(joininfo);
 	WRITE_BOOL_FIELD(has_eclass_joins);
 	WRITE_BITMAPSET_FIELD(top_parent_relids);
+	WRITE_NODE_FIELD(unique_rels);
+	WRITE_NODE_FIELD(non_unique_rels);
 }
 
 static void
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index ed07e2f..f7af7b4 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2081,15 +2081,13 @@ cost_group(Path *path, PlannerInfo *root,
  * 'jointype' is the type of join to be performed
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2120,10 +2118,13 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -2156,17 +2157,16 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  *
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->outerjoinpath;
 	Path	   *inner_path = path->innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	Cost		startup_cost = workspace->startup_cost;
@@ -2206,10 +2206,13 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2350,7 +2353,7 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
  * 'inner_path' is the inner input to the join
  * 'outersortkeys' is the list of sort keys for the outer path
  * 'innersortkeys' is the list of sort keys for the inner path
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  *
  * Note: outersortkeys and innersortkeys should be NIL if no explicit
  * sort is needed because the respective source path is already ordered.
@@ -2361,7 +2364,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo)
+					   JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2438,7 +2441,8 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerstartsel = cache->leftstartsel;
 			innerendsel = cache->leftendsel;
 		}
-		if (jointype == JOIN_LEFT ||
+		if (extra->inner_unique ||
+			jointype == JOIN_LEFT ||
 			jointype == JOIN_ANTI)
 		{
 			outerstartsel = 0.0;
@@ -2576,12 +2580,12 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		materialize_inner
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo)
+					 JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
@@ -2670,9 +2674,14 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * computations?
 	 *
 	 * The whole issue is moot if we are working from a unique-ified outer
-	 * input.
+	 * input. We'll also never rescan during SEMI/ANTI/inner_unique as there's
+	 * no need to advance the inner side to find subsequent matches, therefore
+	 * no need to mark/restore.
 	 */
-	if (IsA(outer_path, UniquePath))
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		IsA(outer_path, UniquePath))
 		rescannedtuples = 0;
 	else
 	{
@@ -2721,7 +2730,11 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	/*
 	 * Even if materializing doesn't look cheaper, we *must* do it if the
 	 * inner path is to be used directly (without sorting) and it doesn't
-	 * support mark/restore.
+	 * support mark/restore. However, if the inner side of the join is unique
+	 * or we're performing a SEMI join, and all of the joinrestrictinfos are
+	 * present as merge clauses, then we may be able to skip mark and restore
+	 * completely.  The logic here must follow the logic that is used to set
+	 * mj_SkipMarkRestore in ExecInitMergeJoin().
 	 *
 	 * Since the inner side must be ordered, and only Sorts and IndexScans can
 	 * create order to begin with, and they both support mark/restore, you
@@ -2735,6 +2748,9 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * it off does not entitle us to deliver an invalid plan.
 	 */
 	else if (innersortkeys == NIL &&
+			 !((extra->inner_unique || path->jpath.jointype == JOIN_SEMI) &&
+			   list_length(path->jpath.joinrestrictinfo) ==
+			   list_length(path->path_mergeclauses)) &&
 			 !ExecSupportsMarkRestore(inner_path))
 		path->materialize_inner = true;
 
@@ -2876,16 +2892,14 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
  * 'hashclauses' is the list of joinclauses to be used as hash clauses
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2970,17 +2984,16 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	List	   *hashclauses = path->path_hashclauses;
@@ -3101,13 +3114,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index de7044d..9fd7b75 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -21,6 +21,7 @@
 #include "optimizer/cost.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/planmain.h"
 
 /* Hook for plugins to get control in add_paths_to_joinrel() */
 set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
@@ -121,6 +122,17 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.  JOIN_UNIQUE_INNER gets a free pass
+	 * because it'll be made unique on the join condition later.
+	 */
+	if (jointype == JOIN_UNIQUE_INNER)
+		extra.inner_unique = true;
+	else
+		extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+												restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -336,8 +348,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * methodology worthwhile.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -348,11 +359,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -399,8 +408,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
 
@@ -410,11 +418,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -486,7 +492,7 @@ try_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -497,10 +503,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -562,7 +567,7 @@ try_partial_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
@@ -573,10 +578,9 @@ try_partial_mergejoin_path(PlannerInfo *root,
 										   joinrel,
 										   jointype,
 										   &workspace,
-										   extra->sjinfo,
+										   extra,
 										   outer_path,
 										   inner_path,
-										   extra->restrictlist,
 										   pathkeys,
 										   NULL,
 										   mergeclauses,
@@ -620,8 +624,7 @@ try_hashjoin_path(PlannerInfo *root,
 	 * never have any output pathkeys, per comments in create_hashjoin_path.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -632,11 +635,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -683,8 +684,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, NIL))
 		return;
 
@@ -694,11 +694,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index ac63f75..7fa8a1b 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -41,6 +41,10 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
 
 
 /*
@@ -845,3 +849,154 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' is unique based on the join
+ *	  condition in 'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   List *restrictlist)
+{
+	MemoryContext	old_context;
+	ListCell	   *lc;
+
+	/* can't prove uniqueness for joins with an empty restrictlist */
+	if (restrictlist == NIL)
+		return false;
+
+	/*
+	 * Currently we're unable to prove uniqueness when there's more than one
+	 * relation on the inner side of the join.
+	 */
+	if (bms_membership(innerrel->relids) != BMS_SINGLETON)
+		return false;
+
+	/*
+	 * First let's query the unique and non-unique caches to see if we've
+	 * managed to prove that innerrel is unique for some subset of this
+	 * outerrel. We don't need an exact match, as if we have any extra
+	 * outerrels than were previously cached, then they can't make the
+	 * innerrel any less unique.
+	 */
+	foreach(lc, innerrel->unique_rels)
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(unique_rels, outerrel->relids))
+			return true;		/* Success! */
+	}
+
+	/*
+	 * We may have previously determined that this outerrel, or some superset
+	 * thereof, cannot prove this innerrel to be unique.
+	 */
+	foreach(lc, innerrel->non_unique_rels)
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(outerrel->relids, unique_rels))
+			return false;
+	}
+
+	/* No cached information, so try to make the proof. */
+	if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+	{
+		/*
+		 * Cache the positive result for future probes, being sure to keep it
+		 * in the planner_cxt even if we are working in GEQO.
+		 *
+		 * Note: one might consider trying to isolate the minimal subset of
+		 * the outerrels that proved the innerrel unique.  But it's not worth
+		 * the trouble, because the planner builds up joinrels incrementally
+		 * and so we'll see the minimally sufficient outerrels before any
+		 * supersets of them anyway.
+		 */
+		old_context = MemoryContextSwitchTo(root->planner_cxt);
+		innerrel->unique_rels = lappend(innerrel->unique_rels,
+					bms_copy(outerrel->relids));
+		MemoryContextSwitchTo(old_context);
+
+		return true;		/* Success! */
+	}
+	else
+	{
+		/*
+		 * None of the join conditions for outerrel proved innerrel unique, so
+		 * we can safely reject this outerrel or any subset of it in future
+		 * checks.
+		 *
+		 * However, in normal planning mode, caching this knowledge is totally
+		 * pointless; it won't be queried again, because we build up joinrels
+		 * from smaller to larger.  It is useful in GEQO mode, where the
+		 * knowledge can be carried across successive planning attempts; and
+		 * it's likely to be useful when using join-search plugins, too.
+		 * Hence cache only when join_search_private is non-NULL.  (Yeah,
+		 * that's a hack, but it seems reasonable.)
+		 */
+		if (root->join_search_private)
+		{
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			innerrel->non_unique_rels = lappend(innerrel->non_unique_rels,
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+
+		return false;
+	}
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index b121f40..a327b48 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -215,12 +215,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -235,7 +235,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3714,7 +3714,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -4016,7 +4016,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -4156,7 +4156,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5349,7 +5349,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5358,9 +5358,10 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5372,7 +5373,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5382,8 +5383,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5424,7 +5426,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5438,8 +5440,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 8536212..a2875dc 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -2049,11 +2049,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -2064,16 +2062,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -2109,7 +2106,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2122,8 +2119,9 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2136,10 +2134,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2152,10 +2149,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2163,6 +2159,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2172,7 +2169,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2185,12 +2182,13 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2202,11 +2200,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2216,15 +2212,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2234,7 +2229,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2259,10 +2254,11 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 3aa701f..a7c8bf9 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -143,6 +143,8 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent)
 	rel->baserestrict_min_security = UINT_MAX;
 	rel->joininfo = NIL;
 	rel->has_eclass_joins = false;
+	rel->unique_rels = NIL;
+	rel->non_unique_rels = NIL;
 
 	/*
 	 * Pass top parent's relids down the inheritance hierarchy. If the parent
@@ -521,6 +523,8 @@ build_join_rel(PlannerInfo *root,
 	joinrel->joininfo = NIL;
 	joinrel->has_eclass_joins = false;
 	joinrel->top_parent_relids = NULL;
+	joinrel->unique_rels = NIL;
+	joinrel->non_unique_rels = NIL;
 
 	/* Compute information relevant to the foreign relations. */
 	set_foreign_rel_properties(joinrel, outer_rel, inner_rel);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index fa99244..66ed681 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1537,6 +1537,9 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	ExprState  *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		first_inner_tuple_only; /* True if we should stop searching
+										 * for more inner tuples after finding
+										 * the first match */
 } JoinState;
 
 /* ----------------
@@ -1567,6 +1570,7 @@ typedef struct NestLoopState
  *		FillInner		   true if should emit unjoined inner tuples anyway
  *		MatchedOuter	   true if found a join match for current outer tuple
  *		MatchedInner	   true if found a join match for current inner tuple
+ *		SkipMarkRestore    true if mark and restore are certainly no-ops
  *		OuterTupleSlot	   slot in tuple table for cur outer tuple
  *		InnerTupleSlot	   slot in tuple table for cur inner tuple
  *		MarkedTupleSlot    slot in tuple table for marked tuple
@@ -1591,6 +1595,7 @@ typedef struct MergeJoinState
 	bool		mj_FillInner;
 	bool		mj_MatchedOuter;
 	bool		mj_MatchedInner;
+	bool		mj_SkipMarkRestore;
 	TupleTableSlot *mj_OuterTupleSlot;
 	TupleTableSlot *mj_InnerTupleSlot;
 	TupleTableSlot *mj_MarkedTupleSlot;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index a2dd26f..b16d04c 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -634,6 +634,8 @@ typedef struct CustomScan
  * jointype:	rule for joining tuples from left and right subtrees
  * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
  *				(plan.qual contains conditions that came from WHERE)
+ * inner_unique each outer tuple can match to no more than one inner
+ *				tuple
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -649,6 +651,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 6bad18e..8447e5a 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -574,6 +574,17 @@ typedef struct RelOptInfo
 
 	/* used by "other" relations. */
 	Relids		top_parent_relids;		/* Relids of topmost parents. */
+
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner side based on the join condition.
+	 * This is a rather expensive test to perform and requires looking for
+	 * proofs such as a relation's unique indexes.  We use these lists during
+	 * the join search to record lists of the sets of relations which both
+	 * prove, and disprove the uniqueness properties for this rel.
+	 */
+	List	  *unique_rels;	/* relids which make this rel a unique join */
+	List	  *non_unique_rels;	/* relids which prove this rel not unique */
 } RelOptInfo;
 
 /*
@@ -1277,6 +1288,9 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* each outer tuple can match to no more than
+								 * one inner tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -2143,6 +2157,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique true if proofs exist which prove that outer side tuples match
+ *		no more than one inner side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2151,6 +2167,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 6909359..ed70def 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -129,33 +129,29 @@ extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void initial_cost_mergejoin(PlannerInfo *root,
 					   JoinCostWorkspace *workspace,
 					   JoinType jointype,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo);
+					   JoinPathExtraData *extra);
 extern void final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo);
+					 JoinPathExtraData *extra);
 extern void initial_cost_hashjoin(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void cost_gather(GatherPath *path, PlannerInfo *root,
 			RelOptInfo *baserel, ParamPathInfo *param_info, double *rows);
 extern void cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 2e712c6..25182f4 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -119,11 +119,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -131,10 +129,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -145,11 +142,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -255,6 +250,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptInfo *parent);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..7ea03ca 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -105,6 +105,9 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   List *restrictlist);
 
 /*
  * prototypes for plan/setrefs.c
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..9784a87 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,7 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +391,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -982,29 +983,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 564218b..a96b2a1 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -317,12 +317,11 @@ explain (costs off)
                      ->  Index Scan using ec1_expr2 on ec1 ec1_1
                      ->  Index Scan using ec1_expr3 on ec1 ec1_2
                      ->  Index Scan using ec1_expr4 on ec1 ec1_3
-               ->  Materialize
-                     ->  Sort
-                           Sort Key: ec1.f1 USING <
-                           ->  Index Scan using ec1_pkey on ec1
-                                 Index Cond: (ff = '42'::bigint)
-(20 rows)
+               ->  Sort
+                     Sort Key: ec1.f1 USING <
+                     ->  Index Scan using ec1_pkey on ec1
+                           Index Cond: (ff = '42'::bigint)
+(19 rows)
 
 -- check partially indexed scan
 set enable_nestloop = on;
@@ -374,12 +373,11 @@ explain (costs off)
                Sort Key: (((ec1_2.ff + 3) + 1))
                ->  Seq Scan on ec1 ec1_2
          ->  Index Scan using ec1_expr4 on ec1 ec1_3
-   ->  Materialize
-         ->  Sort
-               Sort Key: ec1.f1 USING <
-               ->  Index Scan using ec1_pkey on ec1
-                     Index Cond: (ff = '42'::bigint)
-(14 rows)
+   ->  Sort
+         Sort Key: ec1.f1 USING <
+         ->  Index Scan using ec1_pkey on ec1
+               Index Cond: (ff = '42'::bigint)
+(13 rows)
 
 -- check effects of row-level security
 set enable_nestloop = on;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 4992048..fe29353 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3370,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3414,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3426,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3446,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3479,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3491,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3516,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3550,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3562,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3590,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3622,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3642,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3672,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3688,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3713,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3736,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3762,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3778,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3793,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3823,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3850,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3938,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3951,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -3979,7 +4012,7 @@ select id from a where id in (
 );
          QUERY PLAN         
 ----------------------------
- Hash Semi Join
+ Hash Join
    Hash Cond: (a.id = b.id)
    ->  Seq Scan on a
    ->  Hash
@@ -4805,12 +4838,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4837,12 +4871,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4870,6 +4905,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4877,7 +4913,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4897,12 +4933,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4924,10 +4961,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4936,7 +4975,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4982,16 +5021,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5058,13 +5099,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5080,7 +5125,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5098,17 +5143,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5130,7 +5180,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5143,16 +5193,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5165,10 +5217,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5177,7 +5231,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5204,10 +5258,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5229,7 +5285,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5327,3 +5383,315 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop Left Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+                 QUERY PLAN                 
+--------------------------------------------
+ Merge Join
+   Merge Cond: (j1.id1 = j2.id1)
+   Join Filter: (j1.id2 = j2.id2)
+   ->  Index Scan using j1_id1_idx on j1
+   ->  Index Scan using j1_id1_idx on j1 j2
+(5 rows)
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+ id1 | id2 | id1 | id2 
+-----+-----+-----+-----
+   1 |   1 |   1 |   1
+   1 |   2 |   1 |   2
+(2 rows)
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 2beb658..c6bf803 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,13 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(8 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
@@ -5759,6 +5760,7 @@ UPDATE transition_table_base
   WHERE id BETWEEN 2 AND 3;
 INFO:  Hash Full Join
   Output: COALESCE(ot.id, nt.id), ot.val, nt.val
+  Inner Unique: No
   Hash Cond: (ot.id = nt.id)
   ->  Named Tuplestore Scan
         Output: ot.id, ot.val
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 4b6170d..65049a8 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -1991,12 +1991,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2017,11 +2018,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index ed7d6d8..063cb13 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -837,6 +837,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -845,7 +846,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -865,6 +866,7 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -882,7 +884,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index fdcc497..b16c968 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2195,6 +2195,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2202,6 +2203,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2209,6 +2211,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2216,12 +2219,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out
index d3bd8c9..7e0f3cf 100644
--- a/src/test/regress/expected/xml_1.out
+++ b/src/test/regress/expected/xml_1.out
@@ -926,12 +926,13 @@ EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1;
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- XMLNAMESPACES tests
 SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz),
@@ -1068,12 +1069,13 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- test qual
 SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) WHERE "COUNTRY_NAME" = 'Japan';
@@ -1087,13 +1089,14 @@ SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan"
 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
          Table Function Call: XMLTABLE(('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]'::text) PASSING (xmldata.data) COLUMNS "COUNTRY_NAME" text, "REGION_ID" integer)
          Filter: ("xmltable"."COUNTRY_NAME" = 'Japan'::text)
-(8 rows)
+(9 rows)
 
 -- should to work with more data
 INSERT INTO xmldata VALUES('<ROWS>
@@ -1186,13 +1189,14 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
          Filter: ("xmltable".region_id = 2)
-(8 rows)
+(9 rows)
 
 -- should fail, NULL value
 SELECT  xmltable.*
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index cca1a53..7118459 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1732,3 +1732,127 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+
+drop table j1;
+drop table j2;
+drop table j3;
#128Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#127)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:

On 7 April 2017 at 11:47, Tom Lane <tgl@sss.pgh.pa.us> wrote:

What I'm on about is that you can't do the early advance to the
next outer tuple unless you're sure that all the quals that were
relevant to the uniqueness proof have been checked for the current
inner tuple. That affects all three join types not only merge.

Well, look at the join code and you'll see this only happens after the
joinqual is evaulated. I didn't make a special effort here. I just
borrowed the location that JOIN_SEMI was already using.

Right, and that's exactly the point: some of the conditions you're
depending on might have ended up in the otherqual not the joinqual.

We'd discussed rearranging the executor logic enough to deal with
such situations and agreed that it seemed too messy; but that means
that the optimization needs to take care not to use otherqual
(ie pushed-down) conditions in the uniqueness proofs.

Oh yeah. I get it, but that's why we ignore !can_join clauses

can_join seems to me to be not particularly relevant ... there's
nothing that prevents that from getting set for pushed-down clauses.

It's possible that the case I'm worried about is unreachable in
practice because all the conditions that could be of interest would
be strict and therefore would have forced join strength reduction.
But I'm not comfortable with assuming that.

regards, tom lane

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

#129David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#128)
1 attachment(s)
Re: Performance improvement for joins where outer side is unique

On 7 April 2017 at 13:41, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

On 7 April 2017 at 11:47, Tom Lane <tgl@sss.pgh.pa.us> wrote:

What I'm on about is that you can't do the early advance to the
next outer tuple unless you're sure that all the quals that were
relevant to the uniqueness proof have been checked for the current
inner tuple. That affects all three join types not only merge.

Well, look at the join code and you'll see this only happens after the
joinqual is evaulated. I didn't make a special effort here. I just
borrowed the location that JOIN_SEMI was already using.

Right, and that's exactly the point: some of the conditions you're
depending on might have ended up in the otherqual not the joinqual.

We'd discussed rearranging the executor logic enough to deal with
such situations and agreed that it seemed too messy; but that means
that the optimization needs to take care not to use otherqual
(ie pushed-down) conditions in the uniqueness proofs.

Oh yeah. I get it, but that's why we ignore !can_join clauses

can_join seems to me to be not particularly relevant ... there's
nothing that prevents that from getting set for pushed-down clauses.

It's possible that the case I'm worried about is unreachable in
practice because all the conditions that could be of interest would?
be strict and therefore would have forced join strength reduction.
But I'm not comfortable with assuming that.

Okay, well how about we protect against that by not using such quals
as unique proofs? We'd just need to ignore anything that's
outerjoin_delayed?

If we're struggling to think of a case that this will affect, then we
shouldn't be too worried about any missed optimisations.

A patch which does this is attached.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

unique_joins_2017-04-07b.patchapplication/octet-stream; name=unique_joins_2017-04-07b.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a18ab43..009d982 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1343,6 +1343,25 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	if (es->verbose)
 		show_plan_tlist(planstate, ancestors, es);
 
+	/* unique join */
+	if (es->verbose || es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		switch (nodeTag(plan))
+		{
+			case T_NestLoop:
+			case T_MergeJoin:
+			case T_HashJoin:
+				{
+					const char *value = ((Join *) plan)->inner_unique ? "Yes" : "No";
+
+					ExplainPropertyText("Inner Unique", value, es);
+					break;
+				}
+			default:
+				break;
+		}
+	}
+
 	/* quals, sort keys, etc */
 	switch (nodeTag(plan))
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index f2c885a..b73cb90 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -288,10 +288,10 @@ ExecHashJoin(HashJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->hj_JoinState = HJ_NEED_NEW_OUTER;
 
 					if (otherqual == NULL || ExecQual(otherqual, econtext))
@@ -399,6 +399,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	hjstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	hjstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -438,8 +446,14 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			hjstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 62784af..4a2a38d 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -802,10 +802,10 @@ ExecMergeJoin(MergeJoinState *node)
 					}
 
 					/*
-					 * In a semijoin, we'll consider returning the first
-					 * match, but after that we're done with this outer tuple.
+					 * Skip to the next outer tuple if we only need to join to
+					 * the first inner matching tuple.
 					 */
-					if (node->js.jointype == JOIN_SEMI)
+					if (node->js.first_inner_tuple_only)
 						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
 
 					qualResult = (otherqual == NULL ||
@@ -826,7 +826,18 @@ ExecMergeJoin(MergeJoinState *node)
 						InstrCountFiltered2(node, 1);
 				}
 				else
+				{
+					/*
+					 * SkipMarkRestore will be set if we only require a single
+					 * match on the inner side, or if there can only be one
+					 * match. We can safely skip to the next outer tuple in
+					 * this case.
+					 */
+					if (node->mj_SkipMarkRestore)
+						node->mj_JoinState = EXEC_MJ_NEXTOUTER;
+
 					InstrCountFiltered1(node, 1);
+				}
 				break;
 
 				/*
@@ -1048,7 +1059,9 @@ ExecMergeJoin(MergeJoinState *node)
 					/*
 					 * the merge clause matched so now we restore the inner
 					 * scan position to the first mark, and go join that tuple
-					 * (and any following ones) to the new outer.
+					 * (and any following ones) to the new outer.  If we were
+					 * able to determine mark and restore would be a no-op,
+					 * then we can skip this rigmarole.
 					 *
 					 * NOTE: we do not need to worry about the MatchedInner
 					 * state for the rescanned inner tuples.  We know all of
@@ -1062,17 +1075,20 @@ ExecMergeJoin(MergeJoinState *node)
 					 * forcing the merge clause to never match, so we never
 					 * get here.
 					 */
-					ExecRestrPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecRestrPos(innerPlan);
 
-					/*
-					 * ExecRestrPos probably should give us back a new Slot,
-					 * but since it doesn't, use the marked slot.  (The
-					 * previously returned mj_InnerTupleSlot cannot be assumed
-					 * to hold the required tuple.)
-					 */
-					node->mj_InnerTupleSlot = innerTupleSlot;
-					/* we need not do MJEvalInnerValues again */
+						/*
+						 * ExecRestrPos probably should give us back a new
+						 * Slot, but since it doesn't, use the marked slot.
+						 * (The previously returned mj_InnerTupleSlot cannot
+						 * be assumed to hold the required tuple.)
+						 */
+						node->mj_InnerTupleSlot = innerTupleSlot;
+					}
 
+					/* we need not do MJEvalInnerValues again */
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
@@ -1172,9 +1188,12 @@ ExecMergeJoin(MergeJoinState *node)
 
 				if (compareResult == 0)
 				{
-					ExecMarkPos(innerPlan);
+					if (!node->mj_SkipMarkRestore)
+					{
+						ExecMarkPos(innerPlan);
 
-					MarkInnerTuple(node->mj_InnerTupleSlot, node);
+						MarkInnerTuple(node->mj_InnerTupleSlot, node);
+					}
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
@@ -1438,6 +1457,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	mergestate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -1499,8 +1526,14 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			mergestate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			mergestate->mj_FillOuter = false;
 			mergestate->mj_FillInner = false;
 			break;
@@ -1581,6 +1614,36 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	mergestate->mj_InnerTupleSlot = NULL;
 
 	/*
+	 * When matching to the first inner tuple only, as in the case with unique
+	 * and SEMI joins, if the joinqual list is empty (i.e all join conditions
+	 * are present in as merge join clauses), then when we find an inner tuple
+	 * matching the outer tuple, we've no need to keep scanning the inner side
+	 * looking for more tuples matching this outer tuple.  This means we're
+	 * never going to advance the inner side to check for more tuples which
+	 * match the current outer side tuple.  This basically boil down to, any
+	 * mark/restore operations we would perform would be a no-op.  The inner
+	 * side will already be in the correct location.  Here we detect this and
+	 * record that no mark and restore is required during the join.
+	 *
+	 * This mark and restore skipping can provide a good performance boost,
+	 * and it is a fairly common case, so it's certainly worth the cost of the
+	 * additional checks which must be performed during the join.
+	 *
+	 * We could apply this optimization in more casee for unique joins. We'd
+	 * just need to ensure that all of the quals which were used to prove the
+	 * inner side is unique are present in the merge join clauses. Currently
+	 * we've no way to get this information from the planner, but it may be
+	 * worth some exploration for ways to improve this in the future. Here we
+	 * can assume all the unique quals are present as merge join quals by the
+	 * lack of any joinquals.  Further tuning in this area is not possible for
+	 * SEMI joins.
+	 *
+	 * XXX if changing this see logic in final_cost_mergejoin()
+	 */
+	mergestate->mj_SkipMarkRestore = !mergestate->js.joinqual &&
+		mergestate->js.first_inner_tuple_only;
+
+	/*
 	 * initialization successful
 	 */
 	MJ1_printf("ExecInitMergeJoin: %s\n",
diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c
index 53977e0..bd16af1 100644
--- a/src/backend/executor/nodeNestloop.c
+++ b/src/backend/executor/nodeNestloop.c
@@ -219,10 +219,10 @@ ExecNestLoop(NestLoopState *node)
 			}
 
 			/*
-			 * In a semijoin, we'll consider returning the first match, but
-			 * after that we're done with this outer tuple.
+			 * Skip to the next outer tuple if we only need to join to the
+			 * first inner matching tuple.
 			 */
-			if (node->js.jointype == JOIN_SEMI)
+			if (node->js.first_inner_tuple_only)
 				node->nl_NeedNewOuter = true;
 
 			if (otherqual == NULL || ExecQual(otherqual, econtext))
@@ -273,6 +273,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 	nlstate->js.ps.state = estate;
 
 	/*
+	 * When the planner was able to determine that the inner side of the join
+	 * will at most contain a single tuple for each outer tuple, then we can
+	 * optimize the join by skipping to the next outer tuple after we find the
+	 * first matching inner tuple.
+	 */
+	nlstate->js.first_inner_tuple_only = node->join.inner_unique;
+
+	/*
 	 * Miscellaneous initialization
 	 *
 	 * create expression context for node
@@ -311,8 +319,14 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags)
 
 	switch (node->join.jointype)
 	{
-		case JOIN_INNER:
 		case JOIN_SEMI:
+
+			/*
+			 * We need to skip to the next outer after having found an inner
+			 * matching tuple with SEMI joins
+			 */
+			nlstate->js.first_inner_tuple_only = true;
+		case JOIN_INNER:
 			break;
 		case JOIN_LEFT:
 		case JOIN_ANTI:
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 1367297..c8bf3a4 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2251,6 +2251,8 @@ _outRelOptInfo(StringInfo str, const RelOptInfo *node)
 	WRITE_NODE_FIELD(joininfo);
 	WRITE_BOOL_FIELD(has_eclass_joins);
 	WRITE_BITMAPSET_FIELD(top_parent_relids);
+	WRITE_NODE_FIELD(unique_rels);
+	WRITE_NODE_FIELD(non_unique_rels);
 }
 
 static void
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index ed07e2f..f7af7b4 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2081,15 +2081,13 @@ cost_group(Path *path, PlannerInfo *root,
  * 'jointype' is the type of join to be performed
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2120,10 +2118,13 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
 	inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
 	inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost;
 
-	if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		jointype == JOIN_SEMI ||
+		jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * Getting decent estimates requires inspection of the join quals,
 		 * which we choose to postpone to final_cost_nestloop.
@@ -2156,17 +2157,16 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
  *
  * 'path' is already filled in except for the rows and cost fields
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->outerjoinpath;
 	Path	   *inner_path = path->innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	Cost		startup_cost = workspace->startup_cost;
@@ -2206,10 +2206,13 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 
 	/* cost of inner-relation source data (we already dealt with outer rel) */
 
-	if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jointype == JOIN_SEMI ||
+		path->jointype == JOIN_ANTI)
 	{
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 */
 		Cost		inner_run_cost = workspace->inner_run_cost;
 		Cost		inner_rescan_run_cost = workspace->inner_rescan_run_cost;
@@ -2350,7 +2353,7 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
  * 'inner_path' is the inner input to the join
  * 'outersortkeys' is the list of sort keys for the outer path
  * 'innersortkeys' is the list of sort keys for the inner path
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  *
  * Note: outersortkeys and innersortkeys should be NIL if no explicit
  * sort is needed because the respective source path is already ordered.
@@ -2361,7 +2364,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo)
+					   JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2438,7 +2441,8 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 			innerstartsel = cache->leftstartsel;
 			innerendsel = cache->leftendsel;
 		}
-		if (jointype == JOIN_LEFT ||
+		if (extra->inner_unique ||
+			jointype == JOIN_LEFT ||
 			jointype == JOIN_ANTI)
 		{
 			outerstartsel = 0.0;
@@ -2576,12 +2580,12 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		materialize_inner
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo)
+					 JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
@@ -2670,9 +2674,14 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * computations?
 	 *
 	 * The whole issue is moot if we are working from a unique-ified outer
-	 * input.
+	 * input. We'll also never rescan during SEMI/ANTI/inner_unique as there's
+	 * no need to advance the inner side to find subsequent matches, therefore
+	 * no need to mark/restore.
 	 */
-	if (IsA(outer_path, UniquePath))
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI ||
+		IsA(outer_path, UniquePath))
 		rescannedtuples = 0;
 	else
 	{
@@ -2721,7 +2730,11 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	/*
 	 * Even if materializing doesn't look cheaper, we *must* do it if the
 	 * inner path is to be used directly (without sorting) and it doesn't
-	 * support mark/restore.
+	 * support mark/restore. However, if the inner side of the join is unique
+	 * or we're performing a SEMI join, and all of the joinrestrictinfos are
+	 * present as merge clauses, then we may be able to skip mark and restore
+	 * completely.  The logic here must follow the logic that is used to set
+	 * mj_SkipMarkRestore in ExecInitMergeJoin().
 	 *
 	 * Since the inner side must be ordered, and only Sorts and IndexScans can
 	 * create order to begin with, and they both support mark/restore, you
@@ -2735,6 +2748,9 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	 * it off does not entitle us to deliver an invalid plan.
 	 */
 	else if (innersortkeys == NIL &&
+			 !((extra->inner_unique || path->jpath.jointype == JOIN_SEMI) &&
+			   list_length(path->jpath.joinrestrictinfo) ==
+			   list_length(path->path_mergeclauses)) &&
 			 !ExecSupportsMarkRestore(inner_path))
 		path->materialize_inner = true;
 
@@ -2876,16 +2892,14 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
  * 'hashclauses' is the list of joinclauses to be used as hash clauses
  * 'outer_path' is the outer input to the join
  * 'inner_path' is the inner input to the join
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors)
+					  JoinPathExtraData *extra)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -2970,17 +2984,16 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
  * 'path' is already filled in except for the rows and cost fields and
  *		num_batches
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if path->jointype is SEMI or ANTI
+ * 'extra' contains miscellaneous information about the join
  */
 void
 final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors)
+					JoinPathExtraData *extra)
 {
 	Path	   *outer_path = path->jpath.outerjoinpath;
 	Path	   *inner_path = path->jpath.innerjoinpath;
+	SemiAntiJoinFactors *semifactors = &extra->semifactors;
 	double		outer_path_rows = outer_path->rows;
 	double		inner_path_rows = inner_path->rows;
 	List	   *hashclauses = path->path_hashclauses;
@@ -3101,13 +3114,16 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 
 	/* CPU costs */
 
-	if (path->jpath.jointype == JOIN_SEMI || path->jpath.jointype == JOIN_ANTI)
+	if (extra->inner_unique ||
+		path->jpath.jointype == JOIN_SEMI ||
+		path->jpath.jointype == JOIN_ANTI)
 	{
 		double		outer_matched_rows;
 		Selectivity inner_scan_frac;
 
 		/*
-		 * SEMI or ANTI join: executor will stop after first match.
+		 * With inner unique, SEMI or ANTI join, the executor will stop after
+		 * the first match.
 		 *
 		 * For an outer-rel row that has at least one match, we can expect the
 		 * bucket scan to stop after a fraction 1/(match_count+1) of the
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index de7044d..9fd7b75 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -21,6 +21,7 @@
 #include "optimizer/cost.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/planmain.h"
 
 /* Hook for plugins to get control in add_paths_to_joinrel() */
 set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
@@ -121,6 +122,17 @@ add_paths_to_joinrel(PlannerInfo *root,
 	extra.param_source_rels = NULL;
 
 	/*
+	 * Check for proofs which prove that this inner relation cannot cause
+	 * duplicate of outer side tuples.  JOIN_UNIQUE_INNER gets a free pass
+	 * because it'll be made unique on the join condition later.
+	 */
+	if (jointype == JOIN_UNIQUE_INNER)
+		extra.inner_unique = true;
+	else
+		extra.inner_unique = innerrel_is_unique(root, outerrel, innerrel,
+												restrictlist);
+
+	/*
 	 * Find potential mergejoin clauses.  We can skip this if we are not
 	 * interested in doing a mergejoin.  However, mergejoin may be our only
 	 * way of implementing a full outer join, so override enable_mergejoin if
@@ -336,8 +348,7 @@ try_nestloop_path(PlannerInfo *root,
 	 * methodology worthwhile.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -348,11 +359,9 @@ try_nestloop_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  pathkeys,
 									  required_outer));
 	}
@@ -399,8 +408,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_nestloop(root, &workspace, jointype,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
 
@@ -410,11 +418,9 @@ try_partial_nestloop_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  pathkeys,
 										  NULL));
 }
@@ -486,7 +492,7 @@ try_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -497,10 +503,9 @@ try_mergejoin_path(PlannerInfo *root,
 									   joinrel,
 									   jointype,
 									   &workspace,
-									   extra->sjinfo,
+									   extra,
 									   outer_path,
 									   inner_path,
-									   extra->restrictlist,
 									   pathkeys,
 									   required_outer,
 									   mergeclauses,
@@ -562,7 +567,7 @@ try_partial_mergejoin_path(PlannerInfo *root,
 	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
 						   outer_path, inner_path,
 						   outersortkeys, innersortkeys,
-						   extra->sjinfo);
+						   extra);
 
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
 		return;
@@ -573,10 +578,9 @@ try_partial_mergejoin_path(PlannerInfo *root,
 										   joinrel,
 										   jointype,
 										   &workspace,
-										   extra->sjinfo,
+										   extra,
 										   outer_path,
 										   inner_path,
-										   extra->restrictlist,
 										   pathkeys,
 										   NULL,
 										   mergeclauses,
@@ -620,8 +624,7 @@ try_hashjoin_path(PlannerInfo *root,
 	 * never have any output pathkeys, per comments in create_hashjoin_path.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 
 	if (add_path_precheck(joinrel,
 						  workspace.startup_cost, workspace.total_cost,
@@ -632,11 +635,9 @@ try_hashjoin_path(PlannerInfo *root,
 									  joinrel,
 									  jointype,
 									  &workspace,
-									  extra->sjinfo,
-									  &extra->semifactors,
+									  extra,
 									  outer_path,
 									  inner_path,
-									  extra->restrictlist,
 									  required_outer,
 									  hashclauses));
 	}
@@ -683,8 +684,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 	 * cost.  Bail out right away if it looks terrible.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
-						  outer_path, inner_path,
-						  extra->sjinfo, &extra->semifactors);
+						  outer_path, inner_path, extra);
 	if (!add_partial_path_precheck(joinrel, workspace.total_cost, NIL))
 		return;
 
@@ -694,11 +694,9 @@ try_partial_hashjoin_path(PlannerInfo *root,
 										  joinrel,
 										  jointype,
 										  &workspace,
-										  extra->sjinfo,
-										  &extra->semifactors,
+										  extra,
 										  outer_path,
 										  inner_path,
-										  extra->restrictlist,
 										  NULL,
 										  hashclauses));
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index ac63f75..37f5427 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -41,6 +41,10 @@ static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel);
 static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel,
 					List *clause_list);
 static Oid	distinct_col_search(int colno, List *colnos, List *opids);
+static bool is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist);
 
 
 /*
@@ -845,3 +849,155 @@ distinct_col_search(int colno, List *colnos, List *opids)
 	}
 	return InvalidOid;
 }
+
+
+/*
+ * is_innerrel_unique_for
+ *	  Determine if this innerrel can, at most, return a single tuple for each
+ *	  outer tuple, based on the 'restrictlist'.
+ */
+static bool
+is_innerrel_unique_for(PlannerInfo *root,
+					   RelOptInfo *outerrel,
+					   RelOptInfo *innerrel,
+					   List *restrictlist)
+{
+	List	   *clause_list = NIL;
+	ListCell   *lc;
+
+	/* Fall out quickly if we certainly can't prove anything */
+	if (restrictlist == NIL ||
+		!rel_supports_distinctness(root, innerrel))
+		return false;
+
+	/*
+	 * Search for mergejoinable clauses that constrain the inner rel against
+	 * the outer rel.  If an operator is mergejoinable then it behaves like
+	 * equality for some btree opclass, so it's what we want.  The
+	 * mergejoinability test also eliminates clauses containing volatile
+	 * functions, which we couldn't depend on.
+	 */
+	foreach(lc, restrictlist)
+	{
+		RestrictInfo *restrictinfo = (RestrictInfo *) lfirst(lc);
+
+		/* Ignore if it's not a mergejoinable clause */
+		if (!restrictinfo->can_join ||
+			restrictinfo->outerjoin_delayed ||
+			restrictinfo->mergeopfamilies == NIL)
+			continue;			/* not mergejoinable */
+
+		/*
+		 * Check if clause has the form "outer op inner" or "inner op outer",
+		 * and if so mark which side is inner.
+		 */
+		if (!clause_sides_match_join(restrictinfo, outerrel->relids,
+									 innerrel->relids))
+			continue;			/* no good for these input relations */
+
+		/* OK, add to list */
+		clause_list = lappend(clause_list, restrictinfo);
+	}
+
+	/* Let rel_is_distinct_for() do the hard work */
+	return rel_is_distinct_for(root, innerrel, clause_list);
+}
+
+/*
+ * innerrel_is_unique
+ *	  Check for proofs which prove that 'innerrel' is unique based on the join
+ *	  condition in 'restrictlist'.
+ */
+bool
+innerrel_is_unique(PlannerInfo *root,
+				   RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   List *restrictlist)
+{
+	MemoryContext	old_context;
+	ListCell	   *lc;
+
+	/* can't prove uniqueness for joins with an empty restrictlist */
+	if (restrictlist == NIL)
+		return false;
+
+	/*
+	 * Currently we're unable to prove uniqueness when there's more than one
+	 * relation on the inner side of the join.
+	 */
+	if (bms_membership(innerrel->relids) != BMS_SINGLETON)
+		return false;
+
+	/*
+	 * First let's query the unique and non-unique caches to see if we've
+	 * managed to prove that innerrel is unique for some subset of this
+	 * outerrel. We don't need an exact match, as if we have any extra
+	 * outerrels than were previously cached, then they can't make the
+	 * innerrel any less unique.
+	 */
+	foreach(lc, innerrel->unique_rels)
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(unique_rels, outerrel->relids))
+			return true;		/* Success! */
+	}
+
+	/*
+	 * We may have previously determined that this outerrel, or some superset
+	 * thereof, cannot prove this innerrel to be unique.
+	 */
+	foreach(lc, innerrel->non_unique_rels)
+	{
+		Bitmapset  *unique_rels = (Bitmapset *) lfirst(lc);
+
+		if (bms_is_subset(outerrel->relids, unique_rels))
+			return false;
+	}
+
+	/* No cached information, so try to make the proof. */
+	if (is_innerrel_unique_for(root, outerrel, innerrel, restrictlist))
+	{
+		/*
+		 * Cache the positive result for future probes, being sure to keep it
+		 * in the planner_cxt even if we are working in GEQO.
+		 *
+		 * Note: one might consider trying to isolate the minimal subset of
+		 * the outerrels that proved the innerrel unique.  But it's not worth
+		 * the trouble, because the planner builds up joinrels incrementally
+		 * and so we'll see the minimally sufficient outerrels before any
+		 * supersets of them anyway.
+		 */
+		old_context = MemoryContextSwitchTo(root->planner_cxt);
+		innerrel->unique_rels = lappend(innerrel->unique_rels,
+					bms_copy(outerrel->relids));
+		MemoryContextSwitchTo(old_context);
+
+		return true;		/* Success! */
+	}
+	else
+	{
+		/*
+		 * None of the join conditions for outerrel proved innerrel unique, so
+		 * we can safely reject this outerrel or any subset of it in future
+		 * checks.
+		 *
+		 * However, in normal planning mode, caching this knowledge is totally
+		 * pointless; it won't be queried again, because we build up joinrels
+		 * from smaller to larger.  It is useful in GEQO mode, where the
+		 * knowledge can be carried across successive planning attempts; and
+		 * it's likely to be useful when using join-search plugins, too.
+		 * Hence cache only when join_search_private is non-NULL.  (Yeah,
+		 * that's a hack, but it seems reasonable.)
+		 */
+		if (root->join_search_private)
+		{
+			old_context = MemoryContextSwitchTo(root->planner_cxt);
+			innerrel->non_unique_rels = lappend(innerrel->non_unique_rels,
+						bms_copy(outerrel->relids));
+			MemoryContextSwitchTo(old_context);
+		}
+
+		return false;
+	}
+}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index b121f40..a327b48 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -215,12 +215,12 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
 			  List *joinclauses, List *otherclauses, List *nestParams,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static HashJoin *make_hashjoin(List *tlist,
 			  List *joinclauses, List *otherclauses,
 			  List *hashclauses,
 			  Plan *lefttree, Plan *righttree,
-			  JoinType jointype);
+			  JoinPath *jpath);
 static Hash *make_hash(Plan *lefttree,
 		  Oid skewTable,
 		  AttrNumber skewColumn,
@@ -235,7 +235,7 @@ static MergeJoin *make_mergejoin(List *tlist,
 			   int *mergestrategies,
 			   bool *mergenullsfirst,
 			   Plan *lefttree, Plan *righttree,
-			   JoinType jointype);
+			   JoinPath *jpath);
 static Sort *make_sort(Plan *lefttree, int numCols,
 		  AttrNumber *sortColIdx, Oid *sortOperators,
 		  Oid *collations, bool *nullsFirst);
@@ -3714,7 +3714,7 @@ create_nestloop_plan(PlannerInfo *root,
 							  nestParams,
 							  outer_plan,
 							  inner_plan,
-							  best_path->jointype);
+							  best_path);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
@@ -4016,7 +4016,7 @@ create_mergejoin_plan(PlannerInfo *root,
 							   mergenullsfirst,
 							   outer_plan,
 							   inner_plan,
-							   best_path->jpath.jointype);
+							   &best_path->jpath);
 
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -4156,7 +4156,7 @@ create_hashjoin_plan(PlannerInfo *root,
 							  hashclauses,
 							  outer_plan,
 							  (Plan *) hash_plan,
-							  best_path->jpath.jointype);
+							  &best_path->jpath);
 
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -5349,7 +5349,7 @@ make_nestloop(List *tlist,
 			  List *nestParams,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	NestLoop   *node = makeNode(NestLoop);
 	Plan	   *plan = &node->join.plan;
@@ -5358,9 +5358,10 @@ make_nestloop(List *tlist,
 	plan->qual = otherclauses;
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
 	node->nestParams = nestParams;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5372,7 +5373,7 @@ make_hashjoin(List *tlist,
 			  List *hashclauses,
 			  Plan *lefttree,
 			  Plan *righttree,
-			  JoinType jointype)
+			  JoinPath *jpath)
 {
 	HashJoin   *node = makeNode(HashJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5382,8 +5383,9 @@ make_hashjoin(List *tlist,
 	plan->lefttree = lefttree;
 	plan->righttree = righttree;
 	node->hashclauses = hashclauses;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
@@ -5424,7 +5426,7 @@ make_mergejoin(List *tlist,
 			   bool *mergenullsfirst,
 			   Plan *lefttree,
 			   Plan *righttree,
-			   JoinType jointype)
+			   JoinPath *jpath)
 {
 	MergeJoin  *node = makeNode(MergeJoin);
 	Plan	   *plan = &node->join.plan;
@@ -5438,8 +5440,9 @@ make_mergejoin(List *tlist,
 	node->mergeCollations = mergecollations;
 	node->mergeStrategies = mergestrategies;
 	node->mergeNullsFirst = mergenullsfirst;
-	node->join.jointype = jointype;
+	node->join.jointype = jpath->jointype;
 	node->join.joinqual = joinclauses;
+	node->join.inner_unique = jpath->inner_unique;
 
 	return node;
 }
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 8536212..a2875dc 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -2049,11 +2049,9 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  * 'joinrel' is the join relation.
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  *
@@ -2064,16 +2062,15 @@ create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer)
 {
 	NestPath   *pathnode = makeNode(NestPath);
 	Relids		inner_req_outer = PATH_REQ_OUTER(inner_path);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	/*
 	 * If the inner path is parameterized by the outer, we must drop any
@@ -2109,7 +2106,7 @@ create_nestloop_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->path.parallel_aware = false;
@@ -2122,8 +2119,9 @@ create_nestloop_path(PlannerInfo *root,
 	pathnode->outerjoinpath = outer_path;
 	pathnode->innerjoinpath = inner_path;
 	pathnode->joinrestrictinfo = restrict_clauses;
+	pathnode->inner_unique = extra->inner_unique;
 
-	final_cost_nestloop(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_nestloop(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2136,10 +2134,9 @@ create_nestloop_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
- * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'extra' contains various information about the join
  * 'outer_path' is the outer path
  * 'inner_path' is the inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'pathkeys' are the path keys of the new join path
  * 'required_outer' is the set of required outer rels
  * 'mergeclauses' are the RestrictInfo nodes to use as merge clauses
@@ -2152,10 +2149,9 @@ create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -2163,6 +2159,7 @@ create_mergejoin_path(PlannerInfo *root,
 					  List *innersortkeys)
 {
 	MergePath  *pathnode = makeNode(MergePath);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2172,7 +2169,7 @@ create_mergejoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2185,12 +2182,13 @@ create_mergejoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_mergeclauses = mergeclauses;
 	pathnode->outersortkeys = outersortkeys;
 	pathnode->innersortkeys = innersortkeys;
 	/* pathnode->materialize_inner will be set by final_cost_mergejoin */
 
-	final_cost_mergejoin(root, pathnode, workspace, sjinfo);
+	final_cost_mergejoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
@@ -2202,11 +2200,9 @@ create_mergejoin_path(PlannerInfo *root,
  * 'joinrel' is the join relation
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
- * 'sjinfo' is extra info about the join for selectivity estimation
- * 'semifactors' contains valid data if jointype is SEMI or ANTI
+ * 'extra' contains various information about the join
  * 'outer_path' is the cheapest outer path
  * 'inner_path' is the cheapest inner path
- * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
  * 'required_outer' is the set of required outer rels
  * 'hashclauses' are the RestrictInfo nodes to use as hash clauses
  *		(this should be a subset of the restrict_clauses list)
@@ -2216,15 +2212,14 @@ create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses)
 {
 	HashPath   *pathnode = makeNode(HashPath);
+	List	   *restrict_clauses = extra->restrictlist;
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
@@ -2234,7 +2229,7 @@ create_hashjoin_path(PlannerInfo *root,
 								  joinrel,
 								  outer_path,
 								  inner_path,
-								  sjinfo,
+								  extra->sjinfo,
 								  required_outer,
 								  &restrict_clauses);
 	pathnode->jpath.path.parallel_aware = false;
@@ -2259,10 +2254,11 @@ create_hashjoin_path(PlannerInfo *root,
 	pathnode->jpath.outerjoinpath = outer_path;
 	pathnode->jpath.innerjoinpath = inner_path;
 	pathnode->jpath.joinrestrictinfo = restrict_clauses;
+	pathnode->jpath.inner_unique = extra->inner_unique;
 	pathnode->path_hashclauses = hashclauses;
 	/* final_cost_hashjoin will fill in pathnode->num_batches */
 
-	final_cost_hashjoin(root, pathnode, workspace, sjinfo, semifactors);
+	final_cost_hashjoin(root, pathnode, workspace, extra);
 
 	return pathnode;
 }
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 3aa701f..a7c8bf9 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -143,6 +143,8 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent)
 	rel->baserestrict_min_security = UINT_MAX;
 	rel->joininfo = NIL;
 	rel->has_eclass_joins = false;
+	rel->unique_rels = NIL;
+	rel->non_unique_rels = NIL;
 
 	/*
 	 * Pass top parent's relids down the inheritance hierarchy. If the parent
@@ -521,6 +523,8 @@ build_join_rel(PlannerInfo *root,
 	joinrel->joininfo = NIL;
 	joinrel->has_eclass_joins = false;
 	joinrel->top_parent_relids = NULL;
+	joinrel->unique_rels = NIL;
+	joinrel->non_unique_rels = NIL;
 
 	/* Compute information relevant to the foreign relations. */
 	set_foreign_rel_properties(joinrel, outer_rel, inner_rel);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index fa99244..66ed681 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1537,6 +1537,9 @@ typedef struct JoinState
 	PlanState	ps;
 	JoinType	jointype;
 	ExprState  *joinqual;		/* JOIN quals (in addition to ps.qual) */
+	bool		first_inner_tuple_only; /* True if we should stop searching
+										 * for more inner tuples after finding
+										 * the first match */
 } JoinState;
 
 /* ----------------
@@ -1567,6 +1570,7 @@ typedef struct NestLoopState
  *		FillInner		   true if should emit unjoined inner tuples anyway
  *		MatchedOuter	   true if found a join match for current outer tuple
  *		MatchedInner	   true if found a join match for current inner tuple
+ *		SkipMarkRestore    true if mark and restore are certainly no-ops
  *		OuterTupleSlot	   slot in tuple table for cur outer tuple
  *		InnerTupleSlot	   slot in tuple table for cur inner tuple
  *		MarkedTupleSlot    slot in tuple table for marked tuple
@@ -1591,6 +1595,7 @@ typedef struct MergeJoinState
 	bool		mj_FillInner;
 	bool		mj_MatchedOuter;
 	bool		mj_MatchedInner;
+	bool		mj_SkipMarkRestore;
 	TupleTableSlot *mj_OuterTupleSlot;
 	TupleTableSlot *mj_InnerTupleSlot;
 	TupleTableSlot *mj_MarkedTupleSlot;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index a2dd26f..b16d04c 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -634,6 +634,8 @@ typedef struct CustomScan
  * jointype:	rule for joining tuples from left and right subtrees
  * joinqual:	qual conditions that came from JOIN/ON or JOIN/USING
  *				(plan.qual contains conditions that came from WHERE)
+ * inner_unique each outer tuple can match to no more than one inner
+ *				tuple
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -649,6 +651,7 @@ typedef struct Join
 	Plan		plan;
 	JoinType	jointype;
 	List	   *joinqual;		/* JOIN quals (in addition to plan.qual) */
+	bool		inner_unique;
 } Join;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 6bad18e..8447e5a 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -574,6 +574,17 @@ typedef struct RelOptInfo
 
 	/* used by "other" relations. */
 	Relids		top_parent_relids;		/* Relids of topmost parents. */
+
+	/*
+	 * During the join search we attempt to determine which joins can be
+	 * proven to be unique on their inner side based on the join condition.
+	 * This is a rather expensive test to perform and requires looking for
+	 * proofs such as a relation's unique indexes.  We use these lists during
+	 * the join search to record lists of the sets of relations which both
+	 * prove, and disprove the uniqueness properties for this rel.
+	 */
+	List	  *unique_rels;	/* relids which make this rel a unique join */
+	List	  *non_unique_rels;	/* relids which prove this rel not unique */
 } RelOptInfo;
 
 /*
@@ -1277,6 +1288,9 @@ typedef struct JoinPath
 
 	List	   *joinrestrictinfo;		/* RestrictInfos to apply to join */
 
+	bool		inner_unique;	/* each outer tuple can match to no more than
+								 * one inner tuple */
+
 	/*
 	 * See the notes for RelOptInfo and ParamPathInfo to understand why
 	 * joinrestrictinfo is needed in JoinPath, and can't be merged into the
@@ -2143,6 +2157,8 @@ typedef struct SemiAntiJoinFactors
  * sjinfo is extra info about special joins for selectivity estimation
  * semifactors is as shown above (only valid for SEMI or ANTI joins)
  * param_source_rels are OK targets for parameterization of result paths
+ * inner_unique true if proofs exist which prove that outer side tuples match
+ *		no more than one inner side tuple
  */
 typedef struct JoinPathExtraData
 {
@@ -2151,6 +2167,7 @@ typedef struct JoinPathExtraData
 	SpecialJoinInfo *sjinfo;
 	SemiAntiJoinFactors semifactors;
 	Relids		param_source_rels;
+	bool		inner_unique;
 } JoinPathExtraData;
 
 /*
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 6909359..ed70def 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -129,33 +129,29 @@ extern void initial_cost_nestloop(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_nestloop(PlannerInfo *root, NestPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void initial_cost_mergejoin(PlannerInfo *root,
 					   JoinCostWorkspace *workspace,
 					   JoinType jointype,
 					   List *mergeclauses,
 					   Path *outer_path, Path *inner_path,
 					   List *outersortkeys, List *innersortkeys,
-					   SpecialJoinInfo *sjinfo);
+					   JoinPathExtraData *extra);
 extern void final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo);
+					 JoinPathExtraData *extra);
 extern void initial_cost_hashjoin(PlannerInfo *root,
 					  JoinCostWorkspace *workspace,
 					  JoinType jointype,
 					  List *hashclauses,
 					  Path *outer_path, Path *inner_path,
-					  SpecialJoinInfo *sjinfo,
-					  SemiAntiJoinFactors *semifactors);
+					  JoinPathExtraData *extra);
 extern void final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 					JoinCostWorkspace *workspace,
-					SpecialJoinInfo *sjinfo,
-					SemiAntiJoinFactors *semifactors);
+					JoinPathExtraData *extra);
 extern void cost_gather(GatherPath *path, PlannerInfo *root,
 			RelOptInfo *baserel, ParamPathInfo *param_info, double *rows);
 extern void cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 2e712c6..25182f4 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -119,11 +119,9 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 List *pathkeys,
 					 Relids required_outer);
 
@@ -131,10 +129,9 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
-					  SpecialJoinInfo *sjinfo,
+					  JoinPathExtraData *extra,
 					  Path *outer_path,
 					  Path *inner_path,
-					  List *restrict_clauses,
 					  List *pathkeys,
 					  Relids required_outer,
 					  List *mergeclauses,
@@ -145,11 +142,9 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
-					 SpecialJoinInfo *sjinfo,
-					 SemiAntiJoinFactors *semifactors,
+					 JoinPathExtraData *extra,
 					 Path *outer_path,
 					 Path *inner_path,
-					 List *restrict_clauses,
 					 Relids required_outer,
 					 List *hashclauses);
 
@@ -255,6 +250,7 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
  * prototypes for relnode.c
  */
 extern void setup_simple_rel_arrays(PlannerInfo *root);
+extern void setup_unique_join_caches(PlannerInfo *root);
 extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 				 RelOptInfo *parent);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 94ef84b..7ea03ca 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -105,6 +105,9 @@ extern void match_foreign_keys_to_quals(PlannerInfo *root);
 extern List *remove_useless_joins(PlannerInfo *root, List *joinlist);
 extern bool query_supports_distinctness(Query *query);
 extern bool query_is_distinct_for(Query *query, List *colnos, List *opids);
+extern bool innerrel_is_unique(PlannerInfo *root, RelOptInfo *outerrel,
+				   RelOptInfo *innerrel,
+				   List *restrictlist);
 
 /*
  * prototypes for plan/setrefs.c
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 0ff8062..9784a87 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -381,6 +381,7 @@ order by 1, 2;
    Sort Key: s1.s1, s2.s2
    ->  Nested Loop
          Output: s1.s1, s2.s2, (sum((s1.s1 + s2.s2)))
+         Inner Unique: No
          ->  Function Scan on pg_catalog.generate_series s1
                Output: s1.s1
                Function Call: generate_series(1, 3)
@@ -390,7 +391,7 @@ order by 1, 2;
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -982,29 +983,31 @@ explain (costs off) select a,c from t1 group by a,c,d;
 explain (costs off) select *
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
- Group
+                      QUERY PLAN                      
+------------------------------------------------------
+ HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
 from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
 group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
-                      QUERY PLAN                       
--------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
-   ->  Merge Join
-         Merge Cond: ((t1.a = t2.x) AND (t1.b = t2.y))
-         ->  Index Scan using t1_pkey on t1
-         ->  Index Scan using t2_pkey on t2
-(6 rows)
+   ->  Hash Join
+         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
+         ->  Seq Scan on t2
+         ->  Hash
+               ->  Seq Scan on t1
+(7 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out
index 564218b..a96b2a1 100644
--- a/src/test/regress/expected/equivclass.out
+++ b/src/test/regress/expected/equivclass.out
@@ -317,12 +317,11 @@ explain (costs off)
                      ->  Index Scan using ec1_expr2 on ec1 ec1_1
                      ->  Index Scan using ec1_expr3 on ec1 ec1_2
                      ->  Index Scan using ec1_expr4 on ec1 ec1_3
-               ->  Materialize
-                     ->  Sort
-                           Sort Key: ec1.f1 USING <
-                           ->  Index Scan using ec1_pkey on ec1
-                                 Index Cond: (ff = '42'::bigint)
-(20 rows)
+               ->  Sort
+                     Sort Key: ec1.f1 USING <
+                     ->  Index Scan using ec1_pkey on ec1
+                           Index Cond: (ff = '42'::bigint)
+(19 rows)
 
 -- check partially indexed scan
 set enable_nestloop = on;
@@ -374,12 +373,11 @@ explain (costs off)
                Sort Key: (((ec1_2.ff + 3) + 1))
                ->  Seq Scan on ec1 ec1_2
          ->  Index Scan using ec1_expr4 on ec1 ec1_3
-   ->  Materialize
-         ->  Sort
-               Sort Key: ec1.f1 USING <
-               ->  Index Scan using ec1_pkey on ec1
-                     Index Cond: (ff = '42'::bigint)
-(14 rows)
+   ->  Sort
+         Sort Key: ec1.f1 USING <
+         ->  Index Scan using ec1_pkey on ec1
+               Index Cond: (ff = '42'::bigint)
+(13 rows)
 
 -- check effects of row-level security
 set enable_nestloop = on;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 4992048..fe29353 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3362,6 +3362,7 @@ using (join_key);
 --------------------------------------------------------------------------
  Nested Loop Left Join
    Output: "*VALUES*".column1, i1.f1, (666)
+   Inner Unique: No
    Join Filter: ("*VALUES*".column1 = i1.f1)
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1
@@ -3369,12 +3370,13 @@ using (join_key);
          Output: i1.f1, (666)
          ->  Nested Loop Left Join
                Output: i1.f1, 666
+               Inner Unique: No
                ->  Seq Scan on public.int4_tbl i1
                      Output: i1.f1
                ->  Index Only Scan using tenk1_unique2 on public.tenk1 i2
                      Output: i2.unique2
                      Index Cond: (i2.unique2 = i1.f1)
-(14 rows)
+(16 rows)
 
 select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from
   (values (0),(1)) foo1(join_key)
@@ -3412,9 +3414,11 @@ select t1.* from
 ----------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3422,9 +3426,11 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Left Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8.q1 = i8b2.q1)
                            ->  Seq Scan on public.int8_tbl i8
                                  Output: i8.q1, i8.q2
@@ -3440,7 +3446,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(30 rows)
+(34 rows)
 
 select t1.* from
   text_tbl t1
@@ -3473,9 +3479,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3483,12 +3491,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Nested Loop
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
                                  ->  Materialize
@@ -3505,7 +3516,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(34 rows)
+(39 rows)
 
 select t1.* from
   text_tbl t1
@@ -3539,9 +3550,11 @@ select t1.* from
 ----------------------------------------------------------------------------
  Hash Left Join
    Output: t1.f1
+   Inner Unique: No
    Hash Cond: (i8.q2 = i4.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q2
+         Inner Unique: No
          Join Filter: (t1.f1 = '***'::text)
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
@@ -3549,12 +3562,15 @@ select t1.* from
                Output: i8.q2
                ->  Hash Right Join
                      Output: i8.q2
+                     Inner Unique: No
                      Hash Cond: ((NULL::integer) = i8b1.q2)
                      ->  Hash Right Join
                            Output: i8.q2, (NULL::integer)
+                           Inner Unique: No
                            Hash Cond: (i8b2.q1 = i8.q1)
                            ->  Hash Join
                                  Output: i8b2.q1, NULL::integer
+                                 Inner Unique: No
                                  Hash Cond: (i8b2.q1 = i4b2.f1)
                                  ->  Seq Scan on public.int8_tbl i8b2
                                        Output: i8b2.q1, i8b2.q2
@@ -3574,7 +3590,7 @@ select t1.* from
          Output: i4.f1
          ->  Seq Scan on public.int4_tbl i4
                Output: i4.f1
-(37 rows)
+(42 rows)
 
 select t1.* from
   text_tbl t1
@@ -3606,14 +3622,17 @@ select * from
 --------------------------------------------------------
  Nested Loop Left Join
    Output: t1.f1, i8.q1, i8.q2, t2.f1, i4.f1
+   Inner Unique: No
    ->  Seq Scan on public.text_tbl t2
          Output: t2.f1
    ->  Materialize
          Output: i8.q1, i8.q2, i4.f1, t1.f1
          ->  Nested Loop
                Output: i8.q1, i8.q2, i4.f1, t1.f1
+               Inner Unique: No
                ->  Nested Loop Left Join
                      Output: i8.q1, i8.q2, i4.f1
+                     Inner Unique: No
                      Join Filter: (i8.q1 = i4.f1)
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
@@ -3623,7 +3642,7 @@ select * from
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                      Filter: (t1.f1 = 'doh!'::text)
-(19 rows)
+(22 rows)
 
 select * from
   text_tbl t1
@@ -3653,9 +3672,11 @@ where t1.f1 = ss.f1;
 --------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+   Inner Unique: No
    Join Filter: (t1.f1 = t2.f1)
    ->  Nested Loop Left Join
          Output: t1.f1, i8.q1, i8.q2
+         Inner Unique: No
          ->  Seq Scan on public.text_tbl t1
                Output: t1.f1
          ->  Materialize
@@ -3667,7 +3688,7 @@ where t1.f1 = ss.f1;
          Output: (i8.q1), t2.f1
          ->  Seq Scan on public.text_tbl t2
                Output: i8.q1, t2.f1
-(16 rows)
+(18 rows)
 
 select * from
   text_tbl t1
@@ -3692,11 +3713,14 @@ where t1.f1 = ss2.f1;
 -------------------------------------------------------------------
  Nested Loop
    Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1, ((i8.q1)), (t2.f1)
+   Inner Unique: No
    Join Filter: (t1.f1 = (t2.f1))
    ->  Nested Loop
          Output: t1.f1, i8.q1, i8.q2, (i8.q1), t2.f1
+         Inner Unique: No
          ->  Nested Loop Left Join
                Output: t1.f1, i8.q1, i8.q2
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl t1
                      Output: t1.f1
                ->  Materialize
@@ -3712,7 +3736,7 @@ where t1.f1 = ss2.f1;
          Output: ((i8.q1)), (t2.f1)
          ->  Seq Scan on public.text_tbl t3
                Output: (i8.q1), t2.f1
-(22 rows)
+(25 rows)
 
 select * from
   text_tbl t1
@@ -3738,10 +3762,13 @@ where tt1.f1 = ss1.c0;
 ----------------------------------------------------------
  Nested Loop
    Output: 1
+   Inner Unique: No
    ->  Nested Loop Left Join
          Output: tt1.f1, tt4.f1
+         Inner Unique: No
          ->  Nested Loop
                Output: tt1.f1
+               Inner Unique: No
                ->  Seq Scan on public.text_tbl tt1
                      Output: tt1.f1
                      Filter: (tt1.f1 = 'foo'::text)
@@ -3751,6 +3778,7 @@ where tt1.f1 = ss1.c0;
                Output: tt4.f1
                ->  Nested Loop Left Join
                      Output: tt4.f1
+                     Inner Unique: No
                      Join Filter: (tt3.f1 = tt4.f1)
                      ->  Seq Scan on public.text_tbl tt3
                            Output: tt3.f1
@@ -3765,7 +3793,7 @@ where tt1.f1 = ss1.c0;
                Output: (tt4.f1)
                ->  Seq Scan on public.text_tbl tt5
                      Output: tt4.f1
-(29 rows)
+(33 rows)
 
 select 1 from
   text_tbl as tt1
@@ -3795,13 +3823,17 @@ where ss1.c2 = 0;
 ------------------------------------------------------------------------
  Nested Loop
    Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
+   Inner Unique: No
    ->  Hash Join
          Output: i41.f1, i42.f1, i8.q1, i8.q2, i43.f1, 42
+         Inner Unique: No
          Hash Cond: (i41.f1 = i42.f1)
          ->  Nested Loop
                Output: i8.q1, i8.q2, i43.f1, i41.f1
+               Inner Unique: No
                ->  Nested Loop
                      Output: i8.q1, i8.q2, i43.f1
+                     Inner Unique: No
                      ->  Seq Scan on public.int8_tbl i8
                            Output: i8.q1, i8.q2
                            Filter: (i8.q1 = 0)
@@ -3818,7 +3850,7 @@ where ss1.c2 = 0;
          Output: (i41.f1), (i8.q1), (i8.q2), (i42.f1), (i43.f1), ((42))
          ->  Seq Scan on public.text_tbl
                Output: i41.f1, i8.q1, i8.q2, i42.f1, i43.f1, (42)
-(25 rows)
+(29 rows)
 
 select ss2.* from
   int4_tbl i41
@@ -3906,6 +3938,7 @@ explain (verbose, costs off)
 ---------------------------------------------------------
  Merge Left Join
    Output: a.q2, b.q1
+   Inner Unique: No
    Merge Cond: (a.q2 = (COALESCE(b.q1, '1'::bigint)))
    Filter: (COALESCE(b.q1, '1'::bigint) > 0)
    ->  Sort
@@ -3918,7 +3951,7 @@ explain (verbose, costs off)
          Sort Key: (COALESCE(b.q1, '1'::bigint))
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, COALESCE(b.q1, '1'::bigint)
-(14 rows)
+(15 rows)
 
 select a.q2, b.q1
   from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1)
@@ -3979,7 +4012,7 @@ select id from a where id in (
 );
          QUERY PLAN         
 ----------------------------
- Hash Semi Join
+ Hash Join
    Hash Cond: (a.id = b.id)
    ->  Seq Scan on a
    ->  Hash
@@ -4805,12 +4838,13 @@ select * from
 ------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (a.q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, a.q2
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4837,12 +4871,13 @@ select * from
 ------------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, b.q2, (COALESCE(a.q2, '42'::bigint))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Seq Scan on public.int8_tbl b
          Output: b.q1, b.q2, COALESCE(a.q2, '42'::bigint)
          Filter: (a.q2 = b.q1)
-(7 rows)
+(8 rows)
 
 select * from
   int8_tbl a left join
@@ -4870,6 +4905,7 @@ select * from int4_tbl i left join
 -------------------------------------------
  Hash Left Join
    Output: i.f1, j.f1
+   Inner Unique: No
    Hash Cond: (i.f1 = j.f1)
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1
@@ -4877,7 +4913,7 @@ select * from int4_tbl i left join
          Output: j.f1
          ->  Seq Scan on public.int2_tbl j
                Output: j.f1
-(9 rows)
+(10 rows)
 
 select * from int4_tbl i left join
   lateral (select * from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4897,12 +4933,13 @@ select * from int4_tbl i left join
 -------------------------------------
  Nested Loop Left Join
    Output: i.f1, (COALESCE(i.*))
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl i
          Output: i.f1, i.*
    ->  Seq Scan on public.int2_tbl j
          Output: j.f1, COALESCE(i.*)
          Filter: (i.f1 = j.f1)
-(7 rows)
+(8 rows)
 
 select * from int4_tbl i left join
   lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true;
@@ -4924,10 +4961,12 @@ select * from int4_tbl a,
 -------------------------------------------------
  Nested Loop
    Output: a.f1, b.f1, c.q1, c.q2
+   Inner Unique: No
    ->  Seq Scan on public.int4_tbl a
          Output: a.f1
    ->  Hash Left Join
          Output: b.f1, c.q1, c.q2
+         Inner Unique: No
          Hash Cond: (b.f1 = c.q1)
          ->  Seq Scan on public.int4_tbl b
                Output: b.f1
@@ -4936,7 +4975,7 @@ select * from int4_tbl a,
                ->  Seq Scan on public.int8_tbl c
                      Output: c.q1, c.q2
                      Filter: (a.f1 = c.q2)
-(14 rows)
+(16 rows)
 
 select * from int4_tbl a,
   lateral (
@@ -4982,16 +5021,18 @@ select * from
 -------------------------------------------------------------
  Nested Loop Left Join
    Output: a.q1, a.q2, b.q1, c.q1, (LEAST(a.q1, b.q1, c.q1))
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl a
          Output: a.q1, a.q2
    ->  Nested Loop
          Output: b.q1, c.q1, LEAST(a.q1, b.q1, c.q1)
+         Inner Unique: No
          ->  Seq Scan on public.int8_tbl b
                Output: b.q1, b.q2
                Filter: (a.q2 = b.q1)
          ->  Seq Scan on public.int8_tbl c
                Output: c.q1, c.q2
-(11 rows)
+(13 rows)
 
 select * from
   int8_tbl a left join lateral
@@ -5058,13 +5099,17 @@ select * from
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint)), d.q1, (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)), ((COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2)))
+   Inner Unique: No
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE(b.q2, '42'::bigint)), (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Left Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, '42'::bigint))
+                     Inner Unique: No
                      Hash Cond: (a.q2 = b.q1)
                      ->  Seq Scan on public.int8_tbl a
                            Output: a.q1, a.q2
@@ -5080,7 +5125,7 @@ select * from
                      Output: c.q1, c.q2
    ->  Result
          Output: (COALESCE((COALESCE(b.q2, '42'::bigint)), d.q2))
-(24 rows)
+(28 rows)
 
 -- case that breaks the old ph_may_need optimization
 explain (verbose, costs off)
@@ -5098,17 +5143,22 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
 ---------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, i.f1
+   Inner Unique: No
    Join Filter: ((COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2)) > i.f1)
    ->  Hash Right Join
          Output: c.q1, c.q2, a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+         Inner Unique: No
          Hash Cond: (d.q1 = c.q2)
          ->  Nested Loop
                Output: a.q1, a.q2, b.q1, d.q1, (COALESCE((COALESCE(b.q2, (b2.f1)::bigint)), d.q2))
+               Inner Unique: No
                ->  Hash Right Join
                      Output: a.q1, a.q2, b.q1, (COALESCE(b.q2, (b2.f1)::bigint))
+                     Inner Unique: No
                      Hash Cond: (b.q1 = a.q2)
                      ->  Nested Loop
                            Output: b.q1, COALESCE(b.q2, (b2.f1)::bigint)
+                           Inner Unique: No
                            Join Filter: (b.q1 < b2.f1)
                            ->  Seq Scan on public.int8_tbl b
                                  Output: b.q1, b.q2
@@ -5130,7 +5180,7 @@ select c.*,a.*,ss1.q1,ss2.q1,ss3.* from
          Output: i.f1
          ->  Seq Scan on public.int4_tbl i
                Output: i.f1
-(34 rows)
+(39 rows)
 
 -- check processing of postponed quals (bug #9041)
 explain (verbose, costs off)
@@ -5143,16 +5193,18 @@ select * from
 ----------------------------------------------
  Nested Loop Left Join
    Output: (1), (2), (3)
+   Inner Unique: No
    Join Filter: (((3) = (1)) AND ((3) = (2)))
    ->  Nested Loop
          Output: (1), (2)
+         Inner Unique: No
          ->  Result
                Output: 1
          ->  Result
                Output: 2
    ->  Result
          Output: 3
-(11 rows)
+(13 rows)
 
 -- check we don't try to do a unique-ified semijoin with LATERAL
 explain (verbose, costs off)
@@ -5165,10 +5217,12 @@ select * from
 ----------------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, "*VALUES*".column2, int4_tbl.f1
+   Inner Unique: No
    ->  Values Scan on "*VALUES*"
          Output: "*VALUES*".column1, "*VALUES*".column2
    ->  Nested Loop Semi Join
          Output: int4_tbl.f1
+         Inner Unique: No
          Join Filter: (int4_tbl.f1 = tenk1.unique1)
          ->  Seq Scan on public.int4_tbl
                Output: int4_tbl.f1
@@ -5177,7 +5231,7 @@ select * from
                ->  Index Scan using tenk1_unique2 on public.tenk1
                      Output: tenk1.unique1
                      Index Cond: (tenk1.unique2 = "*VALUES*".column2)
-(14 rows)
+(16 rows)
 
 select * from
   (values (0,9998), (1,1000)) v(id,x),
@@ -5204,10 +5258,12 @@ lateral (select * from int8_tbl t1,
 -----------------------------------------------------------------
  Nested Loop
    Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl t1
          Output: t1.q1, t1.q2
    ->  Nested Loop
          Output: "*VALUES*".column1, ss2.q1, ss2.q2
+         Inner Unique: No
          ->  Values Scan on "*VALUES*"
                Output: "*VALUES*".column1
          ->  Subquery Scan on ss2
@@ -5229,7 +5285,7 @@ lateral (select * from int8_tbl t1,
                              ->  Seq Scan on public.int8_tbl t3
                                    Output: t3.q1, t3.q2
                                    Filter: (t3.q2 = $2)
-(27 rows)
+(29 rows)
 
 select * from (values (0), (1)) v(id),
 lateral (select * from int8_tbl t1,
@@ -5327,3 +5383,315 @@ ERROR:  invalid reference to FROM-clause entry for table "xx1"
 LINE 1: ...xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
                                                                 ^
 HINT:  There is an entry for table "xx1", but it cannot be referenced from this part of the query.
+--
+-- test planner's ability to mark joins as unique
+--
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   Join Filter: (j1.id > j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id, j3.id
+   Inner Unique: No
+   Hash Cond: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j3.id
+         ->  Seq Scan on public.j3
+               Output: j3.id
+(10 rows)
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Left Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j2.id = j1.id)
+   ->  Seq Scan on public.j2
+         Output: j2.id
+   ->  Hash
+         Output: j1.id
+         ->  Seq Scan on public.j1
+               Output: j1.id
+(10 rows)
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+            QUERY PLAN             
+-----------------------------------
+ Hash Full Join
+   Output: j1.id, j2.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+            QUERY PLAN             
+-----------------------------------
+ Nested Loop
+   Output: j1.id, j2.id
+   Inner Unique: No
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(9 rows)
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+            QUERY PLAN             
+-----------------------------------
+ Hash Join
+   Output: j1.id
+   Inner Unique: Yes
+   Hash Cond: (j1.id = j2.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Hash
+         Output: j2.id
+         ->  Seq Scan on public.j2
+               Output: j2.id
+(10 rows)
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Unique
+               Output: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(15 rows)
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Output: j1.id, j3.id
+   Inner Unique: Yes
+   Join Filter: (j1.id = j3.id)
+   ->  Seq Scan on public.j1
+         Output: j1.id
+   ->  Materialize
+         Output: j3.id
+         ->  Group
+               Output: j3.id
+               Group Key: j3.id
+               ->  Sort
+                     Output: j3.id
+                     Sort Key: j3.id
+                     ->  Seq Scan on public.j3
+                           Output: j3.id
+(16 rows)
+
+drop table j1;
+drop table j2;
+drop table j3;
+-- test a more complex permutations of unique joins
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+analyze j1;
+analyze j2;
+analyze j3;
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+(8 rows)
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: Yes
+   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+   ->  Materialize
+         Output: j2.id1, j2.id2
+         ->  Seq Scan on public.j2
+               Output: j2.id1, j2.id2
+(10 rows)
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+                QUERY PLAN                
+------------------------------------------
+ Nested Loop Left Join
+   Output: j1.id1, j1.id2, j2.id1, j2.id2
+   Inner Unique: No
+   Join Filter: (j1.id1 = j2.id1)
+   ->  Seq Scan on public.j1
+         Output: j1.id1, j1.id2
+         Filter: (j1.id2 = 1)
+   ->  Seq Scan on public.j2
+         Output: j2.id1, j2.id2
+(9 rows)
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+                 QUERY PLAN                 
+--------------------------------------------
+ Merge Join
+   Merge Cond: (j1.id1 = j2.id1)
+   Join Filter: (j1.id2 = j2.id2)
+   ->  Index Scan using j1_id1_idx on j1
+   ->  Index Scan using j1_id1_idx on j1 j2
+(5 rows)
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+ id1 | id2 | id1 | id2 
+-----+-----+-----+-----
+   1 |   1 |   1 |   1
+   1 |   2 |   1 |   2
+(2 rows)
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+drop table j1;
+drop table j2;
+drop table j3;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 2beb658..c6bf803 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5391,12 +5391,13 @@ select i, a from
 -----------------------------------------------------------------
  Nested Loop
    Output: i.i, (returns_rw_array(1))
+   Inner Unique: No
    ->  Result
          Output: returns_rw_array(1)
    ->  Function Scan on public.consumes_rw_array i
          Output: i.i
          Function Call: consumes_rw_array((returns_rw_array(1)))
-(7 rows)
+(8 rows)
 
 select i, a from
   (select returns_rw_array(1) as a offset 0) ss,
@@ -5759,6 +5760,7 @@ UPDATE transition_table_base
   WHERE id BETWEEN 2 AND 3;
 INFO:  Hash Full Join
   Output: COALESCE(ot.id, nt.id), ot.val, nt.val
+  Inner Unique: No
   Hash Cond: (ot.id = nt.id)
   ->  Named Tuplestore Scan
         Output: ot.id, ot.val
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 4b6170d..65049a8 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -1991,12 +1991,13 @@ select x from int8_tbl, extractq2(int8_tbl) f(x);
 ------------------------------------------
  Nested Loop
    Output: f.x
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.q1, int8_tbl.q2
    ->  Function Scan on f
          Output: f.x
          Function Call: int8_tbl.q2
-(7 rows)
+(8 rows)
 
 select x from int8_tbl, extractq2(int8_tbl) f(x);
          x         
@@ -2017,11 +2018,12 @@ select x from int8_tbl, extractq2_2(int8_tbl) f(x);
 -----------------------------------
  Nested Loop
    Output: ((int8_tbl.*).q2)
+   Inner Unique: No
    ->  Seq Scan on public.int8_tbl
          Output: int8_tbl.*
    ->  Result
          Output: (int8_tbl.*).q2
-(6 rows)
+(7 rows)
 
 select x from int8_tbl, extractq2_2(int8_tbl) f(x);
          x         
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index ed7d6d8..063cb13 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -837,6 +837,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
+   Inner Unique: No
    Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
@@ -845,7 +846,7 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on public.tenk1 a
            Output: a.unique1
-(10 rows)
+(11 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
@@ -865,6 +866,7 @@ select * from int4_tbl o where (f1, f1) in
 -------------------------------------------------------------------
  Nested Loop Semi Join
    Output: o.f1
+   Inner Unique: No
    Join Filter: (o.f1 = "ANY_subquery".f1)
    ->  Seq Scan on public.int4_tbl o
          Output: o.f1
@@ -882,7 +884,7 @@ select * from int4_tbl o where (f1, f1) in
                                  Group Key: i.f1
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,2) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index fdcc497..b16c968 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2195,6 +2195,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                  Output: '42'::bigint, '47'::bigint
    ->  Nested Loop
          Output: a.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (a.aa = wcte.q2)
          ->  Seq Scan on public.a
                Output: a.ctid, a.aa
@@ -2202,6 +2203,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: b.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (b.aa = wcte.q2)
          ->  Seq Scan on public.b
                Output: b.ctid, b.aa
@@ -2209,6 +2211,7 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: c.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (c.aa = wcte.q2)
          ->  Seq Scan on public.c
                Output: c.ctid, c.aa
@@ -2216,12 +2219,13 @@ DELETE FROM a USING wcte WHERE aa = q2;
                Output: wcte.*, wcte.q2
    ->  Nested Loop
          Output: d.ctid, wcte.*
+         Inner Unique: No
          Join Filter: (d.aa = wcte.q2)
          ->  Seq Scan on public.d
                Output: d.ctid, d.aa
          ->  CTE Scan on wcte
                Output: wcte.*, wcte.q2
-(38 rows)
+(42 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out
index d3bd8c9..7e0f3cf 100644
--- a/src/test/regress/expected/xml_1.out
+++ b/src/test/regress/expected/xml_1.out
@@ -926,12 +926,13 @@ EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1;
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- XMLNAMESPACES tests
 SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz),
@@ -1068,12 +1069,13 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
-(7 rows)
+(8 rows)
 
 -- test qual
 SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) WHERE "COUNTRY_NAME" = 'Japan';
@@ -1087,13 +1089,14 @@ SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan"
 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable"."COUNTRY_NAME", "xmltable"."REGION_ID"
          Table Function Call: XMLTABLE(('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]'::text) PASSING (xmldata.data) COLUMNS "COUNTRY_NAME" text, "REGION_ID" integer)
          Filter: ("xmltable"."COUNTRY_NAME" = 'Japan'::text)
-(8 rows)
+(9 rows)
 
 -- should to work with more data
 INSERT INTO xmldata VALUES('<ROWS>
@@ -1186,13 +1189,14 @@ SELECT  xmltable.*
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop
    Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
+   Inner Unique: No
    ->  Seq Scan on public.xmldata
          Output: xmldata.data
    ->  Table Function Scan on "xmltable"
          Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name
          Table Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text))
          Filter: ("xmltable".region_id = 2)
-(8 rows)
+(9 rows)
 
 -- should fail, NULL value
 SELECT  xmltable.*
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index cca1a53..7118459 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1732,3 +1732,127 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1)
 delete from xx1 using (select * from int4_tbl where f1 = x1) ss;
 delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss;
 delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss;
+
+--
+-- test planner's ability to mark joins as unique
+--
+
+create table j1 (id int primary key);
+create table j2 (id int primary key);
+create table j3 (id int);
+
+insert into j1 values(1),(2),(3);
+insert into j2 values(1),(2),(3);
+insert into j3 values(1),(1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure join is properly marked as unique
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id = j2.id;
+
+-- ensure join is not unique when not an equi-join
+explain (verbose, costs off)
+select * from j1 inner join j2 on j1.id > j2.id;
+
+-- ensure join is not unique, as j3 has no unique index or pk on id
+explain (verbose, costs off)
+select * from j1 inner join j3 on j1.id = j3.id;
+
+-- ensure left join is marked as unique
+explain (verbose, costs off)
+select * from j1 left join j2 on j1.id = j2.id;
+
+-- ensure right join is marked as unique
+explain (verbose, costs off)
+select * from j1 right join j2 on j1.id = j2.id;
+
+-- ensure full join is marked as unique
+explain (verbose, costs off)
+select * from j1 full join j2 on j1.id = j2.id;
+
+-- a clauseless (cross) join can't be unique
+explain (verbose, costs off)
+select * from j1 cross join j2;
+
+-- ensure a natural join is marked as unique
+explain (verbose, costs off)
+select * from j1 natural join j2;
+
+-- ensure a distinct clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select distinct id from j3) j3 on j1.id = j3.id;
+
+-- ensure group by clause allows the inner to become unique
+explain (verbose, costs off)
+select * from j1
+inner join (select id from j3 group by id) j3 on j1.id = j3.id;
+
+drop table j1;
+drop table j2;
+drop table j3;
+
+-- test a more complex permutations of unique joins
+
+create table j1 (id1 int, id2 int, primary key(id1,id2));
+create table j2 (id1 int, id2 int, primary key(id1,id2));
+create table j3 (id1 int, id2 int, primary key(id1,id2));
+
+insert into j1 values(1,1),(1,2);
+insert into j2 values(1,1);
+insert into j3 values(1,1);
+
+analyze j1;
+analyze j2;
+analyze j3;
+
+-- ensure there's no unique join when not all columns which are part of the
+-- unique index are seen in the join clause
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1;
+
+-- ensure proper unique detection with multiple join quals
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
+
+-- ensure we don't detect the join to be unique when quals are not part of the
+-- join condition
+explain (verbose, costs off)
+select * from j1
+inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- as above, but for left joins.
+explain (verbose, costs off)
+select * from j1
+left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
+
+-- validate logic in merge joins which skips mark and restore.
+-- it should only do this if all quals which were used to detect the unique
+-- are present as join quals, and not plain quals.
+set enable_nestloop to 0;
+set enable_hashjoin to 0;
+set enable_sort to 0;
+
+-- create an index that will be preferred of the PK to perform the join
+create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1;
+
+explain (costs off) select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+select * from j1 j1
+inner join j1 j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
+where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
+
+reset enable_nestloop;
+reset enable_hashjoin;
+reset enable_sort;
+
+drop table j1;
+drop table j2;
+drop table j3;
#130Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#129)
Re: Performance improvement for joins where outer side is unique

David Rowley <david.rowley@2ndquadrant.com> writes:
[ unique_joins_2017-04-07b.patch ]

It turned out that this patch wasn't as close to committable as I'd
thought, but after a full day of whacking at it, I got to a place
where I thought it was OK. So, pushed.

[ and that's a wrap for v10 feature freeze, I think ]

regards, tom lane

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

#131David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#130)
Re: Performance improvement for joins where outer side is unique

On 8 April 2017 at 14:23, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:
[ unique_joins_2017-04-07b.patch ]

It turned out that this patch wasn't as close to committable as I'd
thought, but after a full day of whacking at it, I got to a place
where I thought it was OK. So, pushed.

Many thanks for taking the time to do this, and committing too!

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, 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