diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 477b9f7..334793c 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -76,13 +76,9 @@ static void set_rel_size(PlannerInfo *root, RelOptInfo *rel,
 			 Index rti, RangeTblEntry *rte);
 static void set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
 				 Index rti, RangeTblEntry *rte);
-static void set_plain_rel_size(PlannerInfo *root, RelOptInfo *rel,
-				   RangeTblEntry *rte);
 static void create_plain_partial_paths(PlannerInfo *root, RelOptInfo *rel);
 static void set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
 						  RangeTblEntry *rte);
-static void set_plain_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
-					   RangeTblEntry *rte);
 static void set_tablesample_rel_size(PlannerInfo *root, RelOptInfo *rel,
 						 RangeTblEntry *rte);
 static void set_tablesample_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
@@ -366,7 +362,7 @@ set_rel_size(PlannerInfo *root, RelOptInfo *rel,
 				else
 				{
 					/* Plain relation */
-					set_plain_rel_size(root, rel, rte);
+					set_plain_rel_size(root, rel);
 				}
 				break;
 			case RTE_SUBQUERY:
@@ -449,7 +445,7 @@ set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
 				else
 				{
 					/* Plain relation */
-					set_plain_rel_pathlist(root, rel, rte);
+					set_plain_rel_pathlist(root, rel);
 				}
 				break;
 			case RTE_SUBQUERY:
@@ -516,8 +512,8 @@ set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
  * set_plain_rel_size
  *	  Set size estimates for a plain relation (no subquery, no inheritance)
  */
-static void
-set_plain_rel_size(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
+void
+set_plain_rel_size(PlannerInfo *root, RelOptInfo *rel)
 {
 	/*
 	 * Test any partial indexes of rel for applicability.  We must do this
@@ -691,8 +687,8 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
  * set_plain_rel_pathlist
  *	  Build access paths for a plain relation (no subquery, no inheritance)
  */
-static void
-set_plain_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
+void
+set_plain_rel_pathlist(PlannerInfo *root, RelOptInfo *rel)
 {
 	Relids		required_outer;
 
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index f295558..d79b3c7 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -2961,7 +2961,7 @@ ec_member_matches_indexcol(PlannerInfo *root, RelOptInfo *rel,
 }
 
 /*
- * relation_has_unique_index_for
+ * relation_get_unique_index_for
  *	  Determine whether the relation provably has at most one row satisfying
  *	  a set of equality conditions, because the conditions constrain all
  *	  columns of some unique index.
@@ -2982,8 +2982,8 @@ ec_member_matches_indexcol(PlannerInfo *root, RelOptInfo *rel,
  * this routine automatically adds in any usable baserestrictinfo clauses.
  * (Note that the passed-in restrictlist will be destructively modified!)
  */
-bool
-relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
+IndexOptInfo *
+relation_get_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
 							  List *restrictlist,
 							  List *exprlist, List *oprlist)
 {
@@ -2993,7 +2993,7 @@ relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
 
 	/* Short-circuit if no indexes... */
 	if (rel->indexlist == NIL)
-		return false;
+		return NULL;
 
 	/*
 	 * Examine the rel's restriction clauses for usable var = const clauses
@@ -3034,7 +3034,7 @@ relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
 
 	/* Short-circuit the easy case */
 	if (restrictlist == NIL && exprlist == NIL)
-		return false;
+		return NULL;
 
 	/* Examine each index of the relation ... */
 	foreach(ic, rel->indexlist)
@@ -3131,10 +3131,10 @@ relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
 
 		/* Matched all columns of this index? */
 		if (c == ind->ncolumns)
-			return true;
+			return ind;
 	}
 
-	return false;
+	return NULL;
 }
 
 /*
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 7008e13..d57ac82 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -15,11 +15,15 @@
 #include "postgres.h"
 
 #include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
 #include "optimizer/clauses.h"
 #include "optimizer/joininfo.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/plancat.h"
+#include "optimizer/predtest.h"
 #include "optimizer/prep.h"
+#include "optimizer/restrictinfo.h"
 #include "partitioning/partbounds.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -747,6 +751,240 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 	return joinrel;
 }
 
+static bool
+has_relation_reference_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, Var))
+		return ((Var *) node)->varno == (Index) (intptr_t) context;
+
+	return expression_tree_walker(node, has_relation_reference_walker, context);
+}
+
+static bool
+has_relation_reference(PathTarget *target, Index relid)
+{
+	return has_relation_reference_walker((Node *) target->exprs,
+										 (void *) (intptr_t) relid);
+}
+
+static bool
+set_varno_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, Var))
+		((Var *) node)->varno = (Index) (intptr_t) context;
+
+	return expression_tree_walker(node, set_varno_walker, context);
+}
+
+static void
+set_varno(Expr *target, Index relid)
+{
+	set_varno_walker((Node *) target, (void*) (intptr_t) relid);
+}
+
+static inline bool
+clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
+						RelOptInfo *innerrel)
+{
+	if (bms_is_subset(rinfo->left_relids, outerrel->relids) &&
+		bms_is_subset(rinfo->right_relids, innerrel->relids))
+	{
+		/* lefthand side is outer */
+		rinfo->outer_is_left = true;
+		return true;
+	}
+	else if (bms_is_subset(rinfo->left_relids, innerrel->relids) &&
+			 bms_is_subset(rinfo->right_relids, outerrel->relids))
+	{
+		/* righthand side is outer */
+		rinfo->outer_is_left = false;
+		return true;
+	}
+	return false;				/* no good for these input relations */
+}
+
+static void
+init_simple_rel(PlannerInfo *root, RelOptInfo *rel, Index relid)
+{
+	RangeTblEntry *rte;
+
+	/* Fetch RTE for relation */
+	rte = root->simple_rte_array[relid];
+	Assert(rte != NULL);
+
+	rel->reloptkind = RELOPT_BASEREL;
+	rel->rtekind = rte->rtekind;
+	rel->rows = 0;
+	rel->relid = relid;
+
+	/* min_attr, max_attr, attr_needed, attr_widths are set below */
+
+	/* Check type of rtable entry */
+	switch (rte->rtekind)
+	{
+		case RTE_RELATION:
+			/* Table --- retrieve statistics from the system catalogs */
+			get_relation_info(root, rte->relid, rte->inh, rel);
+			break;
+		case RTE_SUBQUERY:
+		case RTE_FUNCTION:
+		case RTE_TABLEFUNC:
+		case RTE_VALUES:
+		case RTE_CTE:
+		case RTE_NAMEDTUPLESTORE:
+
+			/*
+			 * Subquery, function, tablefunc, values list, CTE, or ENR --- set
+			 * up attr range and arrays
+			 *
+			 * Note: 0 is included in range to support whole-row Vars
+			 */
+			rel->min_attr = 0;
+			rel->max_attr = list_length(rte->eref->colnames);
+			rel->attr_needed = (Relids *)
+				palloc0((rel->max_attr - rel->min_attr + 1) * sizeof(Relids));
+			rel->attr_widths = (int32 *)
+				palloc0((rel->max_attr - rel->min_attr + 1) * sizeof(int32));
+			break;
+		default:
+			elog(ERROR, "unrecognized RTE kind: %d",
+				 (int) rte->rtekind);
+			break;
+	}
+}
+
+static IndexOptInfo *
+get_inner_unique_index(PlannerInfo *root, RelOptInfo *innerRel,
+							 RelOptInfo *outerRel, List *restrictlist)
+{
+	ListCell *lc;
+	List *rl = list_copy(restrictlist);
+	foreach(lc, rl)
+	{
+		if (!clause_sides_match_join((RestrictInfo *) lfirst(lc),
+									 outerRel, innerRel))
+			return NULL;
+	}
+
+	return relation_get_unique_index_for(root, innerRel, rl, 0, 0);
+}
+
+/*
+ * This function optimizes out an inner join of a relation to itself on a
+ * unique column. When the inner relation is not referenced by the targetlist,
+ * we can transfer the filters from the inner relation to the outer one, and
+ * scan it instead of joining.
+ */
+static bool
+reduce_self_unique_join(PlannerInfo *root, RelOptInfo *outerrel,
+						RelOptInfo *innerrel, RelOptInfo *joinrel,
+						List *restrictlist)
+{
+	List *mergedRinfos;
+	ListCell *lc;
+	IndexOptInfo *outeridx, *inneridx;
+	Bitmapset *oldRelids = joinrel->relids;
+
+	outeridx = get_inner_unique_index(root, outerrel, innerrel, restrictlist);
+	if (!outeridx)
+		return false;
+
+	/*
+	 * Join can be reduced only if the unique constraint is immediate.
+	 * Otherwise, the constraint does not always hold inside a transaction.
+	 */
+	if (!outeridx->immediate)
+		return false;
+
+	inneridx = get_inner_unique_index(root, innerrel, outerrel, restrictlist);
+	if (!inneridx)
+		return false;
+
+	/* We must have the same unique index for both relations. */
+	if (outeridx->indexoid != inneridx->indexoid)
+		return false;
+
+	/* A sanity check: this is the same index on the same relation. */
+	Assert(root->simple_rte_array[outerrel->relid]->relid
+			== root->simple_rte_array[innerrel->relid]->relid);
+
+	/*
+	 * If some columns of the inner relation are in the target list,
+	 * we have to keep it.
+	 */
+	if (has_relation_reference(joinrel->reltarget, innerrel->relid))
+		return false;
+
+	/*
+	 * All the necessary conditions hold and we can change the join
+	 * into a scan on the outer relation. First, append the filters
+	 * from inner relation to the outer.
+	 */
+	mergedRinfos = list_copy(outerrel->baserestrictinfo);
+	foreach(lc, innerrel->baserestrictinfo)
+	{
+		RestrictInfo *oldRinfo = lfirst_node(RestrictInfo, lc);
+		RestrictInfo *newRinfo;
+		Expr *newClause;
+
+		if (is_redundant_derived_clause(oldRinfo, mergedRinfos))
+			continue;			/* derived from same EquivalenceClass */
+
+		newClause = copyObject(oldRinfo->clause);
+		set_varno(newClause, outerrel->relid);
+
+		/*
+		 * !!!FIXME This check doesn't work, because predicate_implied_by
+		 * uses equal() to compare Var nodes. For the Vars transfered from
+		 * another relation, some parameters such as varoattno and location
+		 * are different. They are not meaningful for the purposes of
+		 * Maybe just don't compare them?
+		 */
+		if (!contain_mutable_functions((Node *) newClause)
+			&& predicate_implied_by(list_make1(newClause), mergedRinfos,
+								 /* weak = */ false ))
+			continue;			/* provably implied by r1 */
+
+		/*
+		 * Make a new rinfo instead of copying, because it has quite a few
+		 * internal fields that depend on relid.
+		 */
+		newRinfo = make_restrictinfo(newClause,
+								  oldRinfo->is_pushed_down,
+								  oldRinfo->outerjoin_delayed,
+								  oldRinfo->pseudoconstant,
+								  oldRinfo->security_level,
+								  bms_make_singleton(outerrel->relid) /* required relids */,
+								  NULL /* outer relids */,
+								  oldRinfo->nullable_relids);
+		mergedRinfos = lappend(mergedRinfos, newRinfo);
+	}
+
+	/*
+	 * Pretend that the join relation is a base relation, and
+	 * plan it as usual.
+	 */
+	init_simple_rel(root, joinrel, outerrel->relid);
+
+	joinrel->baserestrictinfo = mergedRinfos;
+	joinrel->relids = bms_make_singleton(joinrel->relid);
+
+	set_plain_rel_size(root, joinrel);
+	set_plain_rel_pathlist(root, joinrel);
+
+	joinrel->relids = oldRelids;
+
+	elog(LOG, "Removed self join.");
+
+	return true;
+}
+
 /*
  * populate_joinrel_with_paths
  *	  Add paths to the given joinrel for given pair of joining relations. The
@@ -786,6 +1024,10 @@ populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1,
 				mark_dummy_rel(joinrel);
 				break;
 			}
+
+			if (reduce_self_unique_join(root, rel1, rel2, joinrel, restrictlist))
+				break;
+
 			add_paths_to_joinrel(root, joinrel, rel1, rel2,
 								 JOIN_INNER, sjinfo,
 								 restrictlist);
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 0e73f9c..128af10 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -601,7 +601,7 @@ rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel)
 		 * reference to unique indexes.  Make sure there's at least one
 		 * suitable unique index.  It must be immediately enforced, and if
 		 * it's a partial index, it must match the query.  (Keep these
-		 * conditions in sync with relation_has_unique_index_for!)
+		 * conditions in sync with relation_get_unique_index_for!)
 		 */
 		ListCell   *lc;
 
@@ -658,10 +658,10 @@ rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list)
 	{
 		/*
 		 * Examine the indexes to see if we have a matching unique index.
-		 * relation_has_unique_index_for automatically adds any usable
+		 * relation_get_unique_index_for automatically adds any usable
 		 * restriction clauses for the rel, so we needn't do that here.
 		 */
-		if (relation_has_unique_index_for(root, rel, clause_list, NIL, NIL))
+		if (relation_get_unique_index_for(root, rel, clause_list, NIL, NIL))
 			return true;
 	}
 	else if (rel->rtekind == RTE_SUBQUERY)
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 0317763..bc9741a 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -4593,9 +4593,17 @@ fix_indexqual_references(PlannerInfo *root, IndexPath *index_path)
 			 * Check to see if the indexkey is on the right; if so, commute
 			 * the clause.  The indexkey should be the side that refers to
 			 * (only) the base relation.
+			 *
+			 * index->rel->relids can be a multi-bit set, so the check
+			 * cannot be simplified to a plain comparison. The multi-bit
+			 * relids are produced by the unique inner join removal, see
+			 * reduce_self_unique_join().
 			 */
-			if (!bms_equal(rinfo->left_relids, index->rel->relids))
+			if (!bms_is_empty(rinfo->right_relids)
+				&& bms_is_subset(rinfo->right_relids, index->rel->relids))
+			{
 				CommuteOpExpr(op);
+			}
 
 			/*
 			 * Now replace the indexkey expression with an index Var.
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index e190ad4..c65474f 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1583,7 +1583,7 @@ create_unique_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
 	 * clauses for the rel, as well.
 	 */
 	if (rel->rtekind == RTE_RELATION && sjinfo->semi_can_btree &&
-		relation_has_unique_index_for(root, rel, NIL,
+		relation_get_unique_index_for(root, rel, NIL,
 									  sjinfo->semi_rhs_exprs,
 									  sjinfo->semi_operators))
 	{
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index cafde30..d2f8bcb 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -50,6 +50,8 @@ extern PGDLLIMPORT join_search_hook_type join_search_hook;
 
 extern RelOptInfo *make_one_rel(PlannerInfo *root, List *joinlist);
 extern void set_dummy_rel_pathlist(RelOptInfo *rel);
+extern void set_plain_rel_size(PlannerInfo *root, RelOptInfo *rel);
+extern void set_plain_rel_pathlist(PlannerInfo *root, RelOptInfo *rel);
 extern RelOptInfo *standard_join_search(PlannerInfo *root, int levels_needed,
 					 List *initial_rels);
 
@@ -71,7 +73,7 @@ extern void debug_print_rel(PlannerInfo *root, RelOptInfo *rel);
  *	  routines to generate index paths
  */
 extern void create_index_paths(PlannerInfo *root, RelOptInfo *rel);
-extern bool relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
+extern IndexOptInfo *relation_get_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
 							  List *restrictlist,
 							  List *exprlist, List *oprlist);
 extern bool indexcol_is_bool_constant_for_query(IndexOptInfo *index,
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index cbc882d..7f3691f 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2918,6 +2918,65 @@ where nt3.id = 1 and ss2.b3;
 (1 row)
 
 --
+-- test removal of semi or inner joins on unique columns
+-- !!!FIXME check coverage
+--
+analyze nt1;
+explain (costs off)
+select a.* from nt1 a
+join (select * from nt1 where a1) b on a.id = b.id;
+    QUERY PLAN     
+-------------------
+ Seq Scan on nt1 a
+   Filter: a1
+(2 rows)
+
+explain (costs off)
+select a.* from nt1 a
+where exists (select * from nt1 where id = a.id) and id < 2;
+     QUERY PLAN     
+--------------------
+ Seq Scan on nt1 a
+   Filter: (id < 2)
+(2 rows)
+
+explain (costs off)
+select a.* from nt1 a
+where exists (select * from nt1 where id = a.id and id < 2);
+     QUERY PLAN     
+--------------------
+ Seq Scan on nt1 a
+   Filter: (id < 2)
+(2 rows)
+
+explain (costs off)
+select a.* from nt1 a
+where exists (select * from nt1 where id = a.id) and a1;
+    QUERY PLAN     
+-------------------
+ Seq Scan on nt1 a
+   Filter: a1
+(2 rows)
+
+explain (costs off)
+select a.* from nt1 a
+where exists (select * from nt1 where id = a.id and a1);
+    QUERY PLAN     
+-------------------
+ Seq Scan on nt1 a
+   Filter: a1
+(2 rows)
+
+explain (costs off)
+select a.* from nt1 a
+where exists (select * from nt1 where id = a.id and a1) and id < 2;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on nt1 a
+   Filter: (a1 AND (id < 2))
+(2 rows)
+
+--
 -- test case where a PlaceHolderVar is propagated into a subquery
 --
 explain (costs off)
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 86c6d5b..1fc6a02 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -851,6 +851,36 @@ from nt3 as nt3
 where nt3.id = 1 and ss2.b3;
 
 --
+-- test removal of semi or inner joins on unique columns
+-- !!!FIXME check coverage
+--
+analyze nt1;
+
+explain (costs off)
+select a.* from nt1 a
+join (select * from nt1 where a1) b on a.id = b.id;
+
+explain (costs off)
+select a.* from nt1 a
+where exists (select * from nt1 where id = a.id) and id < 2;
+
+explain (costs off)
+select a.* from nt1 a
+where exists (select * from nt1 where id = a.id and id < 2);
+
+explain (costs off)
+select a.* from nt1 a
+where exists (select * from nt1 where id = a.id) and a1;
+
+explain (costs off)
+select a.* from nt1 a
+where exists (select * from nt1 where id = a.id and a1);
+
+explain (costs off)
+select a.* from nt1 a
+where exists (select * from nt1 where id = a.id and a1) and id < 2;
+
+--
 -- test case where a PlaceHolderVar is propagated into a subquery
 --
 
