From 638e0d061a22ff5433a6170f6a68610f45187b08 Mon Sep 17 00:00:00 2001
From: Nitin <nitin.jadhav@enterprisedb.com>
Date: Fri, 22 Oct 2021 15:15:22 +0530
Subject: [PATCH] multi column list partitioning

Supported list partitioning based on multiple columns.
Supported new syntax to allow mentioning multiple key information.
Created a infrastructure to accommodate multiple NULL values in
case of list partitioning. Supported partition pruning mechanism
to work for multiple keys. Supported partition-wise join to work
for multiple keys
---
 src/backend/commands/tablecmds.c              |    7 -
 src/backend/executor/execPartition.c          |   10 +-
 src/backend/parser/parse_utilcmd.c            |  192 +++-
 src/backend/partitioning/partbounds.c         |  885 ++++++++++-------
 src/backend/partitioning/partprune.c          |  464 ++++++---
 src/backend/utils/adt/ruleutils.c             |   45 +-
 src/include/partitioning/partbounds.h         |   21 +-
 src/include/utils/ruleutils.h                 |    1 +
 src/test/regress/expected/create_table.out    |   53 +-
 src/test/regress/expected/insert.out          |  147 +++
 src/test/regress/expected/partition_join.out  | 1257 +++++++++++++++++++++++++
 src/test/regress/expected/partition_prune.out |  432 +++++++++
 src/test/regress/sql/create_table.sql         |   35 +-
 src/test/regress/sql/insert.sql               |   64 ++
 src/test/regress/sql/partition_join.sql       |  257 +++++
 src/test/regress/sql/partition_prune.sql      |   42 +
 16 files changed, 3367 insertions(+), 545 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1c2ebe1..77b6519 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16700,13 +16700,6 @@ transformPartitionSpec(Relation rel, PartitionSpec *partspec, char *strategy)
 				 errmsg("unrecognized partitioning strategy \"%s\"",
 						partspec->strategy)));
 
-	/* Check valid number of columns for strategy */
-	if (*strategy == PARTITION_STRATEGY_LIST &&
-		list_length(partspec->partParams) != 1)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-				 errmsg("cannot use \"list\" partition strategy with more than one column")));
-
 	/*
 	 * Create a dummy ParseState and insert the target relation as its sole
 	 * rangetable entry.  We need a ParseState for transformExpr.
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 5c723bc..f7b965a 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -1265,19 +1265,13 @@ get_partition_for_tuple(PartitionDispatch pd, Datum *values, bool *isnull)
 			break;
 
 		case PARTITION_STRATEGY_LIST:
-			if (isnull[0])
-			{
-				if (partition_bound_accepts_nulls(boundinfo))
-					part_index = boundinfo->null_index;
-			}
-			else
 			{
 				bool		equal = false;
 
 				bound_offset = partition_list_bsearch(key->partsupfunc,
 													  key->partcollation,
-													  boundinfo,
-													  values[0], &equal);
+													  boundinfo, values, isnull,
+													  key->partnatts, &equal);
 				if (bound_offset >= 0 && equal)
 					part_index = boundinfo->indexes[bound_offset];
 			}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 313d7b6..acc1543 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -142,6 +142,9 @@ static void validateInfiniteBounds(ParseState *pstate, List *blist);
 static Const *transformPartitionBoundValue(ParseState *pstate, Node *con,
 										   const char *colName, Oid colType, int32 colTypmod,
 										   Oid partCollation);
+static List *transformPartitionListBounds(ParseState *pstate,
+										  PartitionBoundSpec *spec,
+										  Relation parent);
 
 
 /*
@@ -3984,6 +3987,42 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 }
 
 /*
+ * isListBoundDuplicated
+ *
+ * Returns TRUE if the list bound element 'new_bound' is already present
+ * in the target list 'list_bounds', FALSE otherwise.
+ */
+static bool
+isListBoundDuplicated(List *list_bounds, List *new_bound)
+{
+	ListCell   *cell = NULL;
+
+	foreach(cell, list_bounds)
+	{
+		int		i;
+		List   *elem = lfirst(cell);
+		bool	isDuplicate	= true;
+
+		for (i = 0; i < list_length(elem); i++)
+		{
+			Const   *value1 = castNode(Const, list_nth(elem, i));
+			Const   *value2 = castNode(Const, list_nth(new_bound, i));
+
+			if (!equal(value1, value2))
+			{
+				isDuplicate = false;
+				break;
+			}
+		}
+
+		if (isDuplicate)
+			return true;
+	}
+
+	return false;
+}
+
+/*
  * transformPartitionBound
  *
  * Transform a partition bound specification
@@ -3996,7 +4035,6 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 	PartitionKey key = RelationGetPartitionKey(parent);
 	char		strategy = get_partition_strategy(key);
 	int			partnatts = get_partition_natts(key);
-	List	   *partexprs = get_partition_exprs(key);
 
 	/* Avoid scribbling on input */
 	result_spec = copyObject(spec);
@@ -4046,62 +4084,14 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 	}
 	else if (strategy == PARTITION_STRATEGY_LIST)
 	{
-		ListCell   *cell;
-		char	   *colname;
-		Oid			coltype;
-		int32		coltypmod;
-		Oid			partcollation;
-
 		if (spec->strategy != PARTITION_STRATEGY_LIST)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 					 errmsg("invalid bound specification for a list partition"),
 					 parser_errposition(pstate, exprLocation((Node *) spec))));
 
-		/* Get the only column's name in case we need to output an error */
-		if (key->partattrs[0] != 0)
-			colname = get_attname(RelationGetRelid(parent),
-								  key->partattrs[0], false);
-		else
-			colname = deparse_expression((Node *) linitial(partexprs),
-										 deparse_context_for(RelationGetRelationName(parent),
-															 RelationGetRelid(parent)),
-										 false, false);
-		/* Need its type data too */
-		coltype = get_partition_col_typid(key, 0);
-		coltypmod = get_partition_col_typmod(key, 0);
-		partcollation = get_partition_col_collation(key, 0);
-
-		result_spec->listdatums = NIL;
-		foreach(cell, spec->listdatums)
-		{
-			Node	   *expr = lfirst(cell);
-			Const	   *value;
-			ListCell   *cell2;
-			bool		duplicate;
-
-			value = transformPartitionBoundValue(pstate, expr,
-												 colname, coltype, coltypmod,
-												 partcollation);
-
-			/* Don't add to the result if the value is a duplicate */
-			duplicate = false;
-			foreach(cell2, result_spec->listdatums)
-			{
-				Const	   *value2 = lfirst_node(Const, cell2);
-
-				if (equal(value, value2))
-				{
-					duplicate = true;
-					break;
-				}
-			}
-			if (duplicate)
-				continue;
-
-			result_spec->listdatums = lappend(result_spec->listdatums,
-											  value);
-		}
+		result_spec->listdatums =
+			transformPartitionListBounds(pstate, spec, parent);
 	}
 	else if (strategy == PARTITION_STRATEGY_RANGE)
 	{
@@ -4138,6 +4128,106 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 }
 
 /*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw grammar
+ * representation. The result is a List of Lists of Const nodes to account for
+ * the partition key possibly containing more than one column.
+ */
+static List *
+transformPartitionListBounds(ParseState *pstate, PartitionBoundSpec *spec,
+							 Relation parent)
+{
+	int				i;
+	int				j = 0;
+	ListCell	   *cell;
+	List		   *result = NIL;
+	PartitionKey	key = RelationGetPartitionKey(parent);
+	List		   *partexprs = get_partition_exprs(key);
+	int				partnatts = get_partition_natts(key);
+	char		  **colname = (char **) palloc0(partnatts * sizeof(char *));
+	Oid			   *coltype = palloc0(partnatts * sizeof(Oid));
+	int32		   *coltypmod = palloc0(partnatts * sizeof(int));
+	Oid			   *partcollation = palloc0(partnatts * sizeof(Oid));
+
+	for (i = 0; i < partnatts; i++)
+	{
+		if (key->partattrs[i] != 0)
+			colname[i] = get_attname(RelationGetRelid(parent),
+									 key->partattrs[i], false);
+		else
+		{
+			colname[i] =
+				deparse_expression((Node *) list_nth(partexprs, j),
+								   deparse_context_for(RelationGetRelationName(parent),
+													   RelationGetRelid(parent)),
+								   false, false);
+			++j;
+		}
+
+		coltype[i] = get_partition_col_typid(key, i);
+		coltypmod[i] = get_partition_col_typmod(key, i);
+		partcollation[i] = get_partition_col_collation(key, i);
+	}
+
+	foreach(cell, spec->listdatums)
+	{
+		Node	   *expr = lfirst(cell);
+		List	   *values = NIL;
+
+		if (IsA(expr, RowExpr) &&
+			partnatts != list_length(((RowExpr *) expr)->args))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("Must specify exactly one value per partitioning column"),
+					 parser_errposition(pstate, exprLocation((Node *) spec))));
+
+		if (partnatts == 1)
+		{
+			Const	   *val =
+				transformPartitionBoundValue(pstate, expr,colname[0],
+											 coltype[0], coltypmod[0],
+											 partcollation[0]);
+			values = lappend(values, val);
+		}
+		else
+		{
+			ListCell   *cell2;
+			RowExpr		*rowexpr = (RowExpr *) expr;
+
+			if (!IsA(rowexpr, RowExpr))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("Invalid list bound specification"),
+						parser_errposition(pstate, exprLocation((Node *) spec))));
+
+			i = 0;
+			foreach(cell2, rowexpr->args)
+			{
+				Node       *expr = lfirst(cell2);
+				Const      *val =
+					transformPartitionBoundValue(pstate, expr, colname[i],
+												 coltype[i], coltypmod[i],
+												 partcollation[i]);
+				values = lappend(values, val);
+				i++;
+			}
+		}
+
+		/* Don't add to the result if the value is a duplicate */
+		if (!isListBoundDuplicated(result, values))
+			result = lappend(result, values);
+	}
+
+	pfree(colname);
+	pfree(coltype);
+	pfree(coltypmod);
+	pfree(partcollation);
+
+	return result;
+}
+
+/*
  * transformPartitionRangeBounds
  *		This converts the expressions for range partition bounds from the raw
  *		grammar representation to PartitionRangeDatum structs
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index 95798f4..c316ed6 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -53,12 +53,16 @@ typedef struct PartitionHashBound
 	int			index;
 } PartitionHashBound;
 
-/* One value coming from some (index'th) list partition */
-typedef struct PartitionListValue
+/*
+ * One bound of a list partition which belongs to some (index'th) list
+ * partition.
+ */
+typedef struct PartitionListBound
 {
 	int			index;
-	Datum		value;
-} PartitionListValue;
+	Datum	   *values;
+	bool	   *isnulls;
+} PartitionListBound;
 
 /* One bound of a range partition */
 typedef struct PartitionRangeBound
@@ -102,7 +106,8 @@ static PartitionBoundInfo create_list_bounds(PartitionBoundSpec **boundspecs,
 											 int nparts, PartitionKey key, int **mapping);
 static PartitionBoundInfo create_range_bounds(PartitionBoundSpec **boundspecs,
 											  int nparts, PartitionKey key, int **mapping);
-static PartitionBoundInfo merge_list_bounds(FmgrInfo *partsupfunc,
+static PartitionBoundInfo merge_list_bounds(int partnatts,
+											FmgrInfo *partsupfunc,
 											Oid *collations,
 											RelOptInfo *outer_rel,
 											RelOptInfo *inner_rel,
@@ -143,15 +148,14 @@ static int	process_inner_partition(PartitionMap *outer_map,
 									JoinType jointype,
 									int *next_index,
 									int *default_index);
-static void merge_null_partitions(PartitionMap *outer_map,
-								  PartitionMap *inner_map,
-								  bool outer_has_null,
-								  bool inner_has_null,
-								  int outer_null,
-								  int inner_null,
-								  JoinType jointype,
-								  int *next_index,
-								  int *null_index);
+static int merge_null_partitions(PartitionMap *outer_map,
+								   PartitionMap *inner_map,
+								   bool consider_outer_null,
+								   bool consider_inner_null,
+								   int outer_null,
+								   int inner_null,
+								   JoinType jointype,
+								   int *next_index);
 static void merge_default_partitions(PartitionMap *outer_map,
 									 PartitionMap *inner_map,
 									 bool outer_has_default,
@@ -175,6 +179,7 @@ static void generate_matching_part_pairs(RelOptInfo *outer_rel,
 										 List **inner_parts);
 static PartitionBoundInfo build_merged_partition_bounds(char strategy,
 														List *merged_datums,
+														List *merged_isnulls,
 														List *merged_kinds,
 														List *merged_indexes,
 														int null_index,
@@ -365,8 +370,9 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	boundinfo = (PartitionBoundInfoData *)
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
+	boundinfo->partnatts = key->partnatts;
 	/* No special hash partitions. */
-	boundinfo->null_index = -1;
+	boundinfo->isnulls = NULL;
 	boundinfo->default_index = -1;
 
 	hbounds = (PartitionHashBound *)
@@ -438,28 +444,46 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 }
 
 /*
- * get_non_null_list_datum_count
- * 		Counts the number of non-null Datums in each partition.
+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if any of the partition bounds contains a NULL value,
+ * FALSE otherwise.
  */
-static int
-get_non_null_list_datum_count(PartitionBoundSpec **boundspecs, int nparts)
+bool
+partition_bound_accepts_nulls(PartitionBoundInfo boundinfo)
 {
-	int			i;
-	int			count = 0;
+	int i;
 
-	for (i = 0; i < nparts; i++)
+	if (!boundinfo->isnulls)
+		return false;
+
+	for (i = 0; i < boundinfo->ndatums; i++)
 	{
-		ListCell   *lc;
+		int j;
 
-		foreach(lc, boundspecs[i]->listdatums)
+		for (j = 0; j < boundinfo->partnatts; j++)
 		{
-			Const	   *val = lfirst_node(Const, lc);
-
-			if (!val->constisnull)
-				count++;
+			if (boundinfo->isnulls[i][j])
+				return true;
 		}
 	}
 
+	return false;
+}
+
+/*
+ * get_list_datum_count
+ * 		Returns the total number of datums in all the partitions.
+ */
+static int
+get_list_datum_count(PartitionBoundSpec **boundspecs, int nparts)
+{
+	int			i;
+	int			count = 0;
+
+	for (i = 0; i < nparts; i++)
+		count += list_length(boundspecs[i]->listdatums);
+
 	return count;
 }
 
@@ -472,25 +496,25 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 				   PartitionKey key, int **mapping)
 {
 	PartitionBoundInfo boundinfo;
-	PartitionListValue *all_values;
+	PartitionListBound *all_values;
 	int			i;
 	int			j;
 	int			ndatums;
 	int			next_index = 0;
 	int			default_index = -1;
-	int			null_index = -1;
 	Datum	   *boundDatums;
+	bool	   *boundIsNulls;
 
 	boundinfo = (PartitionBoundInfoData *)
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
+	boundinfo->partnatts = key->partnatts;
 	/* Will be set correctly below. */
-	boundinfo->null_index = -1;
 	boundinfo->default_index = -1;
 
-	ndatums = get_non_null_list_datum_count(boundspecs, nparts);
-	all_values = (PartitionListValue *)
-		palloc(ndatums * sizeof(PartitionListValue));
+	ndatums = get_list_datum_count(boundspecs, nparts);
+	all_values = (PartitionListBound *)
+		palloc(ndatums * sizeof(PartitionListBound));
 
 	/* Create a unified list of non-null values across all partitions. */
 	for (j = 0, i = 0; i < nparts; i++)
@@ -514,35 +538,39 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 
 		foreach(c, spec->listdatums)
 		{
-			Const	   *val = lfirst_node(Const, c);
+			int				k = 0;
+			List		   *elem = lfirst(c);
+			ListCell	   *cell;
 
-			if (!val->constisnull)
-			{
-				all_values[j].index = i;
-				all_values[j].value = val->constvalue;
-				j++;
-			}
-			else
+			all_values[j].values = (Datum *) palloc0(key->partnatts * sizeof(Datum));
+			all_values[j].isnulls = (bool *) palloc0(key->partnatts * sizeof(bool));
+			all_values[j].index = i;
+
+			foreach(cell, elem)
 			{
-				/*
-				 * Never put a null into the values array; save the index of
-				 * the partition that stores nulls, instead.
-				 */
-				if (null_index != -1)
-					elog(ERROR, "found null more than once");
-				null_index = i;
+				Const      *val = lfirst_node(Const, cell);
+
+				if (!val->constisnull)
+					all_values[j].values[k] = val->constvalue;
+				else
+					all_values[j].isnulls[k] = true;
+
+				k++;
 			}
+
+			j++;
 		}
 	}
 
 	/* ensure we found a Datum for every slot in the all_values array */
 	Assert(j == ndatums);
 
-	qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+	qsort_arg(all_values, ndatums, sizeof(PartitionListBound),
 			  qsort_partition_list_value_cmp, (void *) key);
 
 	boundinfo->ndatums = ndatums;
 	boundinfo->datums = (Datum **) palloc0(ndatums * sizeof(Datum *));
+	boundinfo->isnulls = (bool **) palloc0(ndatums * sizeof(bool *));
 	boundinfo->kind = NULL;
 	boundinfo->interleaved_parts = NULL;
 	boundinfo->nindexes = ndatums;
@@ -553,7 +581,8 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	 * arrays, here we just allocate a single array and below we'll just
 	 * assign a portion of this array per datum.
 	 */
-	boundDatums = (Datum *) palloc(ndatums * sizeof(Datum));
+	boundDatums = (Datum *) palloc(ndatums * key->partnatts * sizeof(Datum));
+	boundIsNulls = (bool *) palloc(ndatums * key->partnatts * sizeof(bool));
 
 	/*
 	 * Copy values.  Canonical indexes are values ranging from 0 to (nparts -
@@ -563,12 +592,21 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	 */
 	for (i = 0; i < ndatums; i++)
 	{
+		int         j;
 		int			orig_index = all_values[i].index;
 
-		boundinfo->datums[i] = &boundDatums[i];
-		boundinfo->datums[i][0] = datumCopy(all_values[i].value,
-											key->parttypbyval[0],
-											key->parttyplen[0]);
+		boundinfo->datums[i] = &boundDatums[i * key->partnatts];
+		boundinfo->isnulls[i] = &boundIsNulls[i * key->partnatts];
+
+		for (j = 0; j < key->partnatts; j++)
+		{
+			if (!all_values[i].isnulls[j])
+				boundinfo->datums[i][j] = datumCopy(all_values[i].values[j],
+													key->parttypbyval[j],
+													key->parttyplen[j]);
+
+			boundinfo->isnulls[i][j] = all_values[i].isnulls[j];
+		}
 
 		/* If the old index has no mapping, assign one */
 		if ((*mapping)[orig_index] == -1)
@@ -579,22 +617,6 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 
 	pfree(all_values);
 
-	/*
-	 * Set the canonical value for null_index, if any.
-	 *
-	 * It is possible that the null-accepting partition has not been assigned
-	 * an index yet, which could happen if such partition accepts only null
-	 * and hence not handled in the above loop which only looked at non-null
-	 * values.
-	 */
-	if (null_index != -1)
-	{
-		Assert(null_index >= 0);
-		if ((*mapping)[null_index] == -1)
-			(*mapping)[null_index] = next_index++;
-		boundinfo->null_index = (*mapping)[null_index];
-	}
-
 	/* Set the canonical value for default_index, if any. */
 	if (default_index != -1)
 	{
@@ -628,7 +650,6 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 		 * expensive checks to look for interleaved values.
 		 */
 		if (boundinfo->ndatums +
-			partition_bound_accepts_nulls(boundinfo) +
 			partition_bound_has_default(boundinfo) != nparts)
 		{
 			int			last_index = -1;
@@ -646,16 +667,6 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 				if (index < last_index)
 					boundinfo->interleaved_parts = bms_add_member(boundinfo->interleaved_parts,
 																  index);
-
-				/*
-				 * Mark the NULL partition as interleaved if we find that it
-				 * allows some other non-NULL Datum.
-				 */
-				if (partition_bound_accepts_nulls(boundinfo) &&
-					index == boundinfo->null_index)
-					boundinfo->interleaved_parts = bms_add_member(boundinfo->interleaved_parts,
-																  boundinfo->null_index);
-
 				last_index = index;
 			}
 		}
@@ -701,8 +712,8 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	boundinfo = (PartitionBoundInfoData *)
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
-	/* There is no special null-accepting range partition. */
-	boundinfo->null_index = -1;
+	boundinfo->partnatts = key->partnatts;
+	boundinfo->isnulls = NULL;
 	/* Will be set correctly below. */
 	boundinfo->default_index = -1;
 
@@ -905,6 +916,8 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 					   PartitionBoundInfo b1, PartitionBoundInfo b2)
 {
 	int			i;
+	bool		b1_isnull = false;
+	bool		b2_isnull = false;
 
 	if (b1->strategy != b2->strategy)
 		return false;
@@ -915,9 +928,6 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 	if (b1->nindexes != b2->nindexes)
 		return false;
 
-	if (b1->null_index != b2->null_index)
-		return false;
-
 	if (b1->default_index != b2->default_index)
 		return false;
 
@@ -988,7 +998,22 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 				 * context.  datumIsEqual() should be simple enough to be
 				 * safe.
 				 */
-				if (!datumIsEqual(b1->datums[i][j], b2->datums[i][j],
+				if (b1->isnulls)
+					b1_isnull = b1->isnulls[i][j];
+				if (b2->isnulls)
+					b2_isnull = b2->isnulls[i][j];
+
+				/*
+				 * If any of the partition bound has NULL value, then check
+				 * equality for the NULL value instead of comparing the datums
+				 * as it does not contain valid value in case of NULL.
+				 */
+				if (b1_isnull || b2_isnull)
+				{
+					if (b1_isnull != b2_isnull)
+						return false;
+				}
+				else if (!datumIsEqual(b1->datums[i][j], b2->datums[i][j],
 								  parttypbyval[j], parttyplen[j]))
 					return false;
 			}
@@ -1026,10 +1051,11 @@ partition_bounds_copy(PartitionBoundInfo src,
 	nindexes = dest->nindexes = src->nindexes;
 	partnatts = key->partnatts;
 
-	/* List partitioned tables have only a single partition key. */
-	Assert(key->strategy != PARTITION_STRATEGY_LIST || partnatts == 1);
-
 	dest->datums = (Datum **) palloc(sizeof(Datum *) * ndatums);
+	if (src->isnulls)
+		dest->isnulls = (bool **) palloc(sizeof(bool *) * ndatums);
+	else
+		dest->isnulls = NULL;
 
 	if (src->kind != NULL)
 	{
@@ -1075,6 +1101,8 @@ partition_bounds_copy(PartitionBoundInfo src,
 		int			j;
 
 		dest->datums[i] = &boundDatums[i * natts];
+		if (src->isnulls)
+			dest->isnulls[i] = (bool *) palloc(sizeof(bool) * natts);
 
 		for (j = 0; j < natts; j++)
 		{
@@ -1092,17 +1120,22 @@ partition_bounds_copy(PartitionBoundInfo src,
 				typlen = key->parttyplen[j];
 			}
 
-			if (dest->kind == NULL ||
-				dest->kind[i][j] == PARTITION_RANGE_DATUM_VALUE)
+			if ((dest->kind == NULL ||
+				 dest->kind[i][j] == PARTITION_RANGE_DATUM_VALUE) &&
+				(key->strategy != PARTITION_STRATEGY_LIST ||
+				 (src->isnulls == NULL || !src->isnulls[i][j])))
 				dest->datums[i][j] = datumCopy(src->datums[i][j],
 											   byval, typlen);
+
+			if (src->isnulls)
+				dest->isnulls[i][j] = src->isnulls[i][j];
+
 		}
 	}
 
 	dest->indexes = (int *) palloc(sizeof(int) * nindexes);
 	memcpy(dest->indexes, src->indexes, sizeof(int) * nindexes);
 
-	dest->null_index = src->null_index;
 	dest->default_index = src->default_index;
 
 	return dest;
@@ -1162,7 +1195,8 @@ partition_bounds_merge(int partnatts,
 			return NULL;
 
 		case PARTITION_STRATEGY_LIST:
-			return merge_list_bounds(partsupfunc,
+			return merge_list_bounds(partnatts,
+									 partsupfunc,
 									 partcollation,
 									 outer_rel,
 									 inner_rel,
@@ -1206,7 +1240,8 @@ partition_bounds_merge(int partnatts,
  * join can't handle.
  */
 static PartitionBoundInfo
-merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
+merge_list_bounds(int partnatts,
+				  FmgrInfo *partsupfunc, Oid *partcollation,
 				  RelOptInfo *outer_rel, RelOptInfo *inner_rel,
 				  JoinType jointype,
 				  List **outer_parts, List **inner_parts)
@@ -1218,8 +1253,6 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 	bool		inner_has_default = partition_bound_has_default(inner_bi);
 	int			outer_default = outer_bi->default_index;
 	int			inner_default = inner_bi->default_index;
-	bool		outer_has_null = partition_bound_accepts_nulls(outer_bi);
-	bool		inner_has_null = partition_bound_accepts_nulls(inner_bi);
 	PartitionMap outer_map;
 	PartitionMap inner_map;
 	int			outer_pos;
@@ -1229,6 +1262,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 	int			default_index = -1;
 	List	   *merged_datums = NIL;
 	List	   *merged_indexes = NIL;
+	List	   *merged_isnulls = NIL;
 
 	Assert(*outer_parts == NIL);
 	Assert(*inner_parts == NIL);
@@ -1266,6 +1300,20 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		int			cmpval;
 		Datum	   *merged_datum = NULL;
 		int			merged_index = -1;
+		bool	   *outer_isnull;
+		bool	   *inner_isnull;
+		bool	   *merged_isnull = NULL;
+		bool        consider_outer_null = false;
+		bool        consider_inner_null = false;
+		bool		outer_has_null = false;
+		bool		inner_has_null = false;
+		int			i;
+
+		if (outer_bi->isnulls && outer_pos < outer_bi->ndatums)
+			outer_isnull = outer_bi->isnulls[outer_pos];
+
+		if (inner_bi->isnulls && inner_pos < inner_bi->ndatums)
+			inner_isnull = inner_bi->isnulls[inner_pos];
 
 		if (outer_pos < outer_bi->ndatums)
 		{
@@ -1300,6 +1348,26 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		inner_datums = inner_pos < inner_bi->ndatums ?
 			inner_bi->datums[inner_pos] : NULL;
 
+		for (i = 0; i < partnatts; i++)
+		{
+			if (outer_isnull[i])
+			{
+				outer_has_null = true;
+				if (outer_map.merged_indexes[outer_index] == -1)
+					consider_outer_null = true;
+			}
+		}
+
+		for (i = 0; i < partnatts; i++)
+		{
+			if (inner_isnull[i])
+			{
+				inner_has_null = true;
+				if (inner_map.merged_indexes[inner_index] == -1)
+					consider_inner_null = true;
+			}
+		}
+
 		/*
 		 * We run this loop till both sides finish.  This allows us to avoid
 		 * duplicating code to handle the remaining values on the side which
@@ -1316,10 +1384,10 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		else
 		{
 			Assert(outer_datums != NULL && inner_datums != NULL);
-			cmpval = DatumGetInt32(FunctionCall2Coll(&partsupfunc[0],
-													 partcollation[0],
-													 outer_datums[0],
-													 inner_datums[0]));
+			cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+												outer_datums, outer_isnull,
+												inner_datums, inner_isnull,
+												partnatts);
 		}
 
 		if (cmpval == 0)
@@ -1330,17 +1398,34 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 			Assert(outer_index >= 0);
 			Assert(inner_index >= 0);
 
-			/*
-			 * Try merging both partitions.  If successful, add the list value
-			 * and index of the merged partition below.
-			 */
-			merged_index = merge_matching_partitions(&outer_map, &inner_map,
+			if (outer_has_null && inner_has_null)
+			{
+				/* Merge the NULL partitions. */
+				merged_index = merge_null_partitions(&outer_map, &inner_map,
+													 consider_outer_null,
+													 consider_inner_null,
 													 outer_index, inner_index,
-													 &next_index);
-			if (merged_index == -1)
-				goto cleanup;
+													 jointype, &next_index);
+
+				if (merged_index == -1)
+					goto cleanup;
+			}
+			else
+			{
+				/*
+				 * Try merging both partitions.  If successful, add the list
+				 * value and index of the merged partition below.
+				 */
+				merged_index = merge_matching_partitions(&outer_map, &inner_map,
+														 outer_index, inner_index,
+														 &next_index);
+
+				if (merged_index == -1)
+					goto cleanup;
+			}
 
 			merged_datum = outer_datums;
+			merged_isnull = outer_isnull;
 
 			/* Move to the next pair of list values. */
 			outer_pos++;
@@ -1351,14 +1436,30 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 			/* A list value missing from the inner side. */
 			Assert(outer_pos < outer_bi->ndatums);
 
-			/*
-			 * If the inner side has the default partition, or this is an
-			 * outer join, try to assign a merged partition to the outer
-			 * partition (see process_outer_partition()).  Otherwise, the
-			 * outer partition will not contribute to the result.
-			 */
-			if (inner_has_default || IS_OUTER_JOIN(jointype))
+			if (outer_has_null || inner_has_null)
 			{
+				if (consider_outer_null || consider_inner_null)
+				{
+					/* Merge the NULL partitions. */
+					merged_index = merge_null_partitions(&outer_map, &inner_map,
+														 consider_outer_null,
+														 consider_inner_null,
+														 outer_index, inner_index,
+														 jointype, &next_index);
+
+					if (merged_index == -1)
+						goto cleanup;
+				}
+			}
+			else if (inner_has_default || IS_OUTER_JOIN(jointype))
+			{
+				/*
+				 * If the inner side has the default partition, or this is an
+				 * outer join, try to assign a merged partition to the outer
+				 * partition (see process_outer_partition()).  Otherwise, the
+				 * outer partition will not contribute to the result.
+				 */
+
 				/* Get the outer partition. */
 				outer_index = outer_bi->indexes[outer_pos];
 				Assert(outer_index >= 0);
@@ -1373,9 +1474,11 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 													   &default_index);
 				if (merged_index == -1)
 					goto cleanup;
-				merged_datum = outer_datums;
 			}
 
+			merged_datum = outer_datums;
+			merged_isnull = outer_isnull;
+
 			/* Move to the next list value on the outer side. */
 			outer_pos++;
 		}
@@ -1385,14 +1488,30 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 			Assert(cmpval > 0);
 			Assert(inner_pos < inner_bi->ndatums);
 
-			/*
-			 * If the outer side has the default partition, or this is a FULL
-			 * join, try to assign a merged partition to the inner partition
-			 * (see process_inner_partition()).  Otherwise, the inner
-			 * partition will not contribute to the result.
-			 */
-			if (outer_has_default || jointype == JOIN_FULL)
+			if (outer_has_null || inner_has_null)
+			{
+				if (consider_outer_null || consider_inner_null)
+				{
+					/* Merge the NULL partitions. */
+					merged_index = merge_null_partitions(&outer_map, &inner_map,
+														 consider_outer_null,
+														 consider_inner_null,
+														 outer_index, inner_index,
+														 jointype, &next_index);
+
+					if (merged_index == -1)
+						goto cleanup;
+				}
+			}
+			else if (outer_has_default || jointype == JOIN_FULL)
 			{
+				/*
+				 * If the outer side has the default partition, or this is a
+				 * FULL join, try to assign a merged partition to the inner
+				 * partition (see process_inner_partition()).  Otherwise, the
+				 * innerpartition will not contribute to the result.
+				 */
+
 				/* Get the inner partition. */
 				inner_index = inner_bi->indexes[inner_pos];
 				Assert(inner_index >= 0);
@@ -1407,9 +1526,11 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 													   &default_index);
 				if (merged_index == -1)
 					goto cleanup;
-				merged_datum = inner_datums;
 			}
 
+			merged_datum = inner_datums;
+			merged_isnull = inner_isnull;
+
 			/* Move to the next list value on the inner side. */
 			inner_pos++;
 		}
@@ -1422,29 +1543,10 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		{
 			merged_datums = lappend(merged_datums, merged_datum);
 			merged_indexes = lappend_int(merged_indexes, merged_index);
+			merged_isnulls = lappend(merged_isnulls, merged_isnull);
 		}
 	}
 
-	/*
-	 * If the NULL partitions (if any) have been proven empty, deem them
-	 * non-existent.
-	 */
-	if (outer_has_null &&
-		is_dummy_partition(outer_rel, outer_bi->null_index))
-		outer_has_null = false;
-	if (inner_has_null &&
-		is_dummy_partition(inner_rel, inner_bi->null_index))
-		inner_has_null = false;
-
-	/* Merge the NULL partitions if any. */
-	if (outer_has_null || inner_has_null)
-		merge_null_partitions(&outer_map, &inner_map,
-							  outer_has_null, inner_has_null,
-							  outer_bi->null_index, inner_bi->null_index,
-							  jointype, &next_index, &null_index);
-	else
-		Assert(null_index == -1);
-
 	/* Merge the default partitions if any. */
 	if (outer_has_default || inner_has_default)
 		merge_default_partitions(&outer_map, &inner_map,
@@ -1478,6 +1580,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		/* Make a PartitionBoundInfo struct to return. */
 		merged_bounds = build_merged_partition_bounds(outer_bi->strategy,
 													  merged_datums,
+													  merged_isnulls,
 													  NIL,
 													  merged_indexes,
 													  null_index,
@@ -1488,6 +1591,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 cleanup:
 	/* Free local memory before returning. */
 	list_free(merged_datums);
+	list_free(merged_isnulls);
 	list_free(merged_indexes);
 	free_partition_map(&outer_map);
 	free_partition_map(&inner_map);
@@ -1796,6 +1900,7 @@ merge_range_bounds(int partnatts, FmgrInfo *partsupfuncs,
 		/* Make a PartitionBoundInfo struct to return. */
 		merged_bounds = build_merged_partition_bounds(outer_bi->strategy,
 													  merged_datums,
+													  NIL,
 													  merged_kinds,
 													  merged_indexes,
 													  -1,
@@ -2154,48 +2259,24 @@ process_inner_partition(PartitionMap *outer_map,
  * be mergejoinable, and we currently assume that mergejoinable operators are
  * strict (see MJEvalOuterValues()/MJEvalInnerValues()).
  */
-static void
+static int
 merge_null_partitions(PartitionMap *outer_map,
 					  PartitionMap *inner_map,
-					  bool outer_has_null,
-					  bool inner_has_null,
+					  bool consider_outer_null,
+					  bool consider_inner_null,
 					  int outer_null,
 					  int inner_null,
 					  JoinType jointype,
-					  int *next_index,
-					  int *null_index)
+					  int *next_index)
 {
-	bool		consider_outer_null = false;
-	bool		consider_inner_null = false;
-
-	Assert(outer_has_null || inner_has_null);
-	Assert(*null_index == -1);
-
-	/*
-	 * Check whether the NULL partitions have already been merged and if so,
-	 * set the consider_outer_null/consider_inner_null flags.
-	 */
-	if (outer_has_null)
-	{
-		Assert(outer_null >= 0 && outer_null < outer_map->nparts);
-		if (outer_map->merged_indexes[outer_null] == -1)
-			consider_outer_null = true;
-	}
-	if (inner_has_null)
-	{
-		Assert(inner_null >= 0 && inner_null < inner_map->nparts);
-		if (inner_map->merged_indexes[inner_null] == -1)
-			consider_inner_null = true;
-	}
+	int         merged_index = *next_index;
 
 	/* If both flags are set false, we don't need to do anything. */
 	if (!consider_outer_null && !consider_inner_null)
-		return;
+		return merged_index;
 
 	if (consider_outer_null && !consider_inner_null)
 	{
-		Assert(outer_has_null);
-
 		/*
 		 * If this is an outer join, the NULL partition on the outer side has
 		 * to be scanned all the way anyway; merge the NULL partition with a
@@ -2207,14 +2288,12 @@ merge_null_partitions(PartitionMap *outer_map,
 		if (IS_OUTER_JOIN(jointype))
 		{
 			Assert(jointype != JOIN_RIGHT);
-			*null_index = merge_partition_with_dummy(outer_map, outer_null,
+			merged_index = merge_partition_with_dummy(outer_map, outer_null,
 													 next_index);
 		}
 	}
 	else if (!consider_outer_null && consider_inner_null)
 	{
-		Assert(inner_has_null);
-
 		/*
 		 * If this is a FULL join, the NULL partition on the inner side has to
 		 * be scanned all the way anyway; merge the NULL partition with a
@@ -2224,14 +2303,12 @@ merge_null_partitions(PartitionMap *outer_map,
 		 * treat it as the NULL partition of the join relation.
 		 */
 		if (jointype == JOIN_FULL)
-			*null_index = merge_partition_with_dummy(inner_map, inner_null,
+			merged_index = merge_partition_with_dummy(inner_map, inner_null,
 													 next_index);
 	}
 	else
 	{
 		Assert(consider_outer_null && consider_inner_null);
-		Assert(outer_has_null);
-		Assert(inner_has_null);
 
 		/*
 		 * If this is an outer join, the NULL partition on the outer side (and
@@ -2249,12 +2326,13 @@ merge_null_partitions(PartitionMap *outer_map,
 		if (IS_OUTER_JOIN(jointype))
 		{
 			Assert(jointype != JOIN_RIGHT);
-			*null_index = merge_matching_partitions(outer_map, inner_map,
+			merged_index = merge_matching_partitions(outer_map, inner_map,
 													outer_null, inner_null,
 													next_index);
-			Assert(*null_index >= 0);
 		}
 	}
+
+	return merged_index;
 }
 
 /*
@@ -2527,8 +2605,9 @@ generate_matching_part_pairs(RelOptInfo *outer_rel, RelOptInfo *inner_rel,
  */
 static PartitionBoundInfo
 build_merged_partition_bounds(char strategy, List *merged_datums,
-							  List *merged_kinds, List *merged_indexes,
-							  int null_index, int default_index)
+							  List *merged_isnulls, List *merged_kinds,
+							  List *merged_indexes, int null_index,
+							  int default_index)
 {
 	PartitionBoundInfo merged_bounds;
 	int			ndatums = list_length(merged_datums);
@@ -2537,8 +2616,17 @@ build_merged_partition_bounds(char strategy, List *merged_datums,
 
 	merged_bounds = (PartitionBoundInfo) palloc(sizeof(PartitionBoundInfoData));
 	merged_bounds->strategy = strategy;
-	merged_bounds->ndatums = ndatums;
 
+	if (merged_isnulls)
+	{
+		merged_bounds->isnulls = (bool **) palloc(sizeof(bool *) * ndatums);
+
+		pos = 0;
+		foreach(lc, merged_isnulls)
+			merged_bounds->isnulls[pos++] = (bool *) lfirst(lc);
+	}
+
+	merged_bounds->ndatums = ndatums;
 	merged_bounds->datums = (Datum **) palloc(sizeof(Datum *) * ndatums);
 	pos = 0;
 	foreach(lc, merged_datums)
@@ -2556,6 +2644,7 @@ build_merged_partition_bounds(char strategy, List *merged_datums,
 		/* There are ndatums+1 indexes in the case of range partitioning. */
 		merged_indexes = lappend_int(merged_indexes, -1);
 		ndatums++;
+		merged_bounds->isnulls = NULL;
 	}
 	else
 	{
@@ -2567,14 +2656,14 @@ build_merged_partition_bounds(char strategy, List *merged_datums,
 	/* interleaved_parts is always NULL for join relations. */
 	merged_bounds->interleaved_parts = NULL;
 
-	Assert(list_length(merged_indexes) == ndatums);
+	Assert(list_length(merged_indexes) == ndatums ||
+		   list_length(merged_indexes) == ndatums - 1);
 	merged_bounds->nindexes = ndatums;
 	merged_bounds->indexes = (int *) palloc(sizeof(int) * ndatums);
 	pos = 0;
 	foreach(lc, merged_indexes)
 		merged_bounds->indexes[pos++] = lfirst_int(lc);
 
-	merged_bounds->null_index = null_index;
 	merged_bounds->default_index = default_index;
 
 	return merged_bounds;
@@ -3074,30 +3163,31 @@ check_new_partition_bound(char *relname, Relation parent,
 
 					foreach(cell, spec->listdatums)
 					{
-						Const	   *val = lfirst_node(Const, cell);
-
-						overlap_location = val->location;
-						if (!val->constisnull)
+						int			i;
+						int         offset = -1;
+						bool        equal = false;
+						List	   *elem = lfirst(cell);
+						Datum	   values[PARTITION_MAX_KEYS];
+						bool	   isnulls[PARTITION_MAX_KEYS];
+
+						for (i = 0; i < key->partnatts; i++)
 						{
-							int			offset;
-							bool		equal;
-
-							offset = partition_list_bsearch(&key->partsupfunc[0],
-															key->partcollation,
-															boundinfo,
-															val->constvalue,
-															&equal);
-							if (offset >= 0 && equal)
-							{
-								overlap = true;
-								with = boundinfo->indexes[offset];
-								break;
-							}
+							Const	   *val = castNode(Const, list_nth(elem, i));
+
+							values[i] = val->constvalue;
+							isnulls[i] = val->constisnull;
+							overlap_location = val->location;
 						}
-						else if (partition_bound_accepts_nulls(boundinfo))
+
+						offset = partition_list_bsearch(key->partsupfunc,
+														key->partcollation,
+														boundinfo, values,
+														isnulls, key->partnatts,
+														&equal);
+						if (offset >= 0 && equal)
 						{
 							overlap = true;
-							with = boundinfo->null_index;
+							with = boundinfo->indexes[offset];
 							break;
 						}
 					}
@@ -3612,6 +3702,48 @@ partition_hbound_cmp(int modulus1, int remainder1, int modulus2, int remainder2)
 }
 
 /*
+ * partition_lbound_datum_cmp
+ *
+ * Return whether list bound value (given by lb_datums and lb_isnulls) is
+ * <, =, or > partition key of a tuple (specified in values and isnulls).
+ *
+ * nvalues gives the number of values provided in the 'values' and 'isnulls'
+ * array.   partsupfunc and partcollation, both arrays of nvalues elements,
+ * give the comparison functions and the collations to be used when comparing.
+ */
+int32
+partition_lbound_datum_cmp(FmgrInfo *partsupfunc, Oid *partcollation,
+						   Datum *lb_datums, bool *lb_isnulls,
+						   Datum *values, bool *isnulls, int nvalues)
+{
+	int		i;
+	int32	cmpval;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		/* This always places NULLs after not-NULLs. */
+		if (lb_isnulls[i])
+		{
+			if (isnulls && isnulls[i])
+				cmpval = 0;		/* NULL "=" NULL */
+			else
+				cmpval = 1;		/* NULL ">" not-NULL */
+		}
+		else if (isnulls && isnulls[i])
+			cmpval = -1;		/* not-NULL "<" NULL */
+		else
+			cmpval = DatumGetInt32(FunctionCall2Coll(&partsupfunc[i],
+													 partcollation[i],
+													 lb_datums[i], values[i]));
+
+		if (cmpval != 0)
+			break;
+	}
+
+	return cmpval;
+}
+
+/*
  * partition_list_bsearch
  *		Returns the index of the greatest bound datum that is less than equal
  * 		to the given value or -1 if all of the bound datums are greater
@@ -3621,8 +3753,8 @@ partition_hbound_cmp(int modulus1, int remainder1, int modulus2, int remainder2)
  */
 int
 partition_list_bsearch(FmgrInfo *partsupfunc, Oid *partcollation,
-					   PartitionBoundInfo boundinfo,
-					   Datum value, bool *is_equal)
+					   PartitionBoundInfo boundinfo, Datum *values,
+					   bool *isnulls, int nvalues, bool *is_equal)
 {
 	int			lo,
 				hi,
@@ -3635,10 +3767,10 @@ partition_list_bsearch(FmgrInfo *partsupfunc, Oid *partcollation,
 		int32		cmpval;
 
 		mid = (lo + hi + 1) / 2;
-		cmpval = DatumGetInt32(FunctionCall2Coll(&partsupfunc[0],
-												 partcollation[0],
-												 boundinfo->datums[mid][0],
-												 value));
+		cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+											boundinfo->datums[mid],
+											boundinfo->isnulls[mid],
+											values, isnulls, nvalues);
 		if (cmpval <= 0)
 		{
 			lo = mid;
@@ -3808,13 +3940,15 @@ qsort_partition_hbound_cmp(const void *a, const void *b)
 static int32
 qsort_partition_list_value_cmp(const void *a, const void *b, void *arg)
 {
-	Datum		val1 = ((PartitionListValue *const) a)->value,
-				val2 = ((PartitionListValue *const) b)->value;
+	Datum	   *vals1 = ((PartitionListBound *const) a)->values;
+	Datum	   *vals2 = ((PartitionListBound *const) b)->values;
+	bool	   *isnull1 = ((PartitionListBound *const) a)->isnulls;
+	bool	   *isnull2 = ((PartitionListBound *const) b)->isnulls;
 	PartitionKey key = (PartitionKey) arg;
 
-	return DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[0],
-										   key->partcollation[0],
-										   val1, val2));
+	return partition_lbound_datum_cmp(key->partsupfunc, key->partcollation,
+									  vals1, isnull1, vals2, isnull2,
+									  key->partnatts);
 }
 
 /*
@@ -3910,15 +4044,10 @@ make_partition_op_expr(PartitionKey key, int keynum,
 	{
 		case PARTITION_STRATEGY_LIST:
 			{
-				List	   *elems = (List *) arg2;
-				int			nelems = list_length(elems);
-
-				Assert(nelems >= 1);
-				Assert(keynum == 0);
-
-				if (nelems > 1 &&
+				if (IsA(arg2, List) && list_length((List *) arg2) > 1 &&
 					!type_is_array(key->parttypid[keynum]))
 				{
+					List	   *elems = (List *) arg2;
 					ArrayExpr  *arrexpr;
 					ScalarArrayOpExpr *saopexpr;
 
@@ -3945,8 +4074,9 @@ make_partition_op_expr(PartitionKey key, int keynum,
 
 					result = (Expr *) saopexpr;
 				}
-				else
+				else if (IsA(arg2, List) && list_length((List *) arg2) > 1)
 				{
+					List	   *elems = (List *) arg2;
 					List	   *elemops = NIL;
 					ListCell   *lc;
 
@@ -3964,7 +4094,18 @@ make_partition_op_expr(PartitionKey key, int keynum,
 						elemops = lappend(elemops, elemop);
 					}
 
-					result = nelems > 1 ? makeBoolExpr(OR_EXPR, elemops, -1) : linitial(elemops);
+					result = makeBoolExpr(OR_EXPR, elemops, -1);
+				}
+				else
+				{
+					result = make_opclause(operoid,
+										   BOOLOID,
+										   false,
+										   arg1,
+										   IsA(arg2, List) ?
+										   linitial((List *) arg2) : arg2,
+										   InvalidOid,
+										   key->partcollation[keynum]);
 				}
 				break;
 			}
@@ -4082,30 +4223,40 @@ static List *
 get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 {
 	PartitionKey key = RelationGetPartitionKey(parent);
-	List	   *result;
-	Expr	   *keyCol;
-	Expr	   *opexpr;
-	NullTest   *nulltest;
+	List	   *result = NIL;
+	Expr	   *datumtest;
+	Expr	   *is_null_test = NULL;
+	List	   *datum_elems = NIL;
 	ListCell   *cell;
-	List	   *elems = NIL;
-	bool		list_has_null = false;
+	bool		key_is_null[PARTITION_MAX_KEYS];
+	int			i,
+				j;
+	Expr      **keyCol = (Expr **) palloc0 (key->partnatts * sizeof(Expr *));
 
-	/*
-	 * Only single-column list partitioning is supported, so we are worried
-	 * only about the partition key with index 0.
-	 */
-	Assert(key->partnatts == 1);
-
-	/* Construct Var or expression representing the partition column */
-	if (key->partattrs[0] != 0)
-		keyCol = (Expr *) makeVar(1,
-								  key->partattrs[0],
-								  key->parttypid[0],
-								  key->parttypmod[0],
-								  key->parttypcoll[0],
-								  0);
-	else
-		keyCol = (Expr *) copyObject(linitial(key->partexprs));
+	/* Set up partition key Vars/expressions. */
+	for (i = 0, j = 0; i < key->partnatts; i++)
+	{
+		if (key->partattrs[i] != 0)
+		{
+			keyCol[i] = (Expr *) makeVar(1,
+										 key->partattrs[i],
+										 key->parttypid[i],
+										 key->parttypmod[i],
+										 key->parttypcoll[i],
+										 0);
+		}
+		else
+		{
+			keyCol[i] = (Expr *) copyObject(list_nth(key->partexprs, j));
+			++j;
+		}
+
+		/*
+		 * While at it, also initialize IS NULL marker for every key.  This is
+		 * set to true if a given key accepts NULL.
+		 */
+		key_is_null[i] = false;
+	}
 
 	/*
 	 * For default list partition, collect datums for all the partitions. The
@@ -4120,113 +4271,195 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 		PartitionBoundInfo boundinfo = pdesc->boundinfo;
 
 		if (boundinfo)
-		{
 			ndatums = boundinfo->ndatums;
 
-			if (partition_bound_accepts_nulls(boundinfo))
-				list_has_null = true;
-		}
-
 		/*
 		 * If default is the only partition, there need not be any partition
 		 * constraint on it.
 		 */
-		if (ndatums == 0 && !list_has_null)
+		if (ndatums == 0 && !partition_bound_accepts_nulls(boundinfo))
 			return NIL;
 
 		for (i = 0; i < ndatums; i++)
 		{
-			Const	   *val;
+			List	   *and_args = NIL;
+			Expr	   *datum_elem = NULL;
 
 			/*
-			 * Construct Const from known-not-null datum.  We must be careful
-			 * to copy the value, because our result has to be able to outlive
-			 * the relcache entry we're copying from.
+			 * For the multi-column case, we must make an BoolExpr that
+			 * ANDs the results of the expressions for various columns,
+			 * where each expression is either an IS NULL test or an
+			 * OpExpr comparing the column against a non-NULL datum.
 			 */
-			val = makeConst(key->parttypid[0],
-							key->parttypmod[0],
-							key->parttypcoll[0],
-							key->parttyplen[0],
-							datumCopy(*boundinfo->datums[i],
-									  key->parttypbyval[0],
-									  key->parttyplen[0]),
-							false,	/* isnull */
-							key->parttypbyval[0]);
-
-			elems = lappend(elems, val);
+			for (j = 0; j < key->partnatts; j++)
+			{
+				Const      *val = NULL;
+
+				if (boundinfo->isnulls[i][j])
+				{
+					NullTest   *nulltest = makeNode(NullTest);
+
+					key_is_null[j] = true;
+
+					nulltest->arg = keyCol[j];
+					nulltest->nulltesttype = IS_NULL;
+					nulltest->argisrow = false;
+					nulltest->location = -1;
+
+					if (key->partnatts > 1)
+						and_args = lappend(and_args, nulltest);
+					else
+						is_null_test = (Expr *) nulltest;
+				}
+				else
+				{
+					val = makeConst(key->parttypid[j],
+									key->parttypmod[j],
+									key->parttypcoll[j],
+									key->parttyplen[j],
+									datumCopy(boundinfo->datums[i][j],
+											  key->parttypbyval[j],
+											  key->parttyplen[j]),
+									false,  /* isnull */
+									key->parttypbyval[j]);
+
+					if (key->partnatts > 1)
+					{
+						Expr *opexpr =
+							make_partition_op_expr(key, j,
+												   BTEqualStrategyNumber,
+												   keyCol[j],
+												   (Expr *) val);
+						and_args = lappend(and_args, opexpr);
+					}
+					else
+						datum_elem = (Expr *) val;
+				}
+			}
+
+			if (list_length(and_args) > 1)
+				datum_elem = makeBoolExpr(AND_EXPR, and_args, -1);
+
+			if (datum_elem)
+				datum_elems = lappend(datum_elems, datum_elem);
 		}
 	}
 	else
 	{
-		/*
-		 * Create list of Consts for the allowed values, excluding any nulls.
-		 */
 		foreach(cell, spec->listdatums)
 		{
-			Const	   *val = lfirst_node(Const, cell);
+			List	   *listbound = (List *) lfirst(cell);
+			ListCell   *cell2;
+			List	   *and_args = NIL;
+			Expr	   *datum_elem = NULL;
 
-			if (val->constisnull)
-				list_has_null = true;
-			else
-				elems = lappend(elems, copyObject(val));
+			/*
+			 * See the comment above regarding the handling for the
+			 * multi-column case.
+			 */
+			j = 0;
+			foreach(cell2, listbound)
+			{
+				Const      *val = castNode(Const, lfirst(cell2));
+
+				if (val->constisnull)
+				{
+					NullTest   *nulltest = makeNode(NullTest);
+
+					key_is_null[j] = true;
+
+					nulltest->arg = keyCol[j];
+					nulltest->nulltesttype = IS_NULL;
+					nulltest->argisrow = false;
+					nulltest->location = -1;
+
+					if (key->partnatts > 1)
+						and_args = lappend(and_args, nulltest);
+					else
+						is_null_test = (Expr *) nulltest;
+				}
+				else
+				{
+					if (key->partnatts > 1)
+					{
+						Expr *opexpr =
+							make_partition_op_expr(key, j,
+												   BTEqualStrategyNumber,
+												   keyCol[j],
+												   (Expr *) val);
+						and_args = lappend(and_args, opexpr);
+					}
+					else
+						datum_elem = (Expr *) val;
+				}
+				j++;
+			}
+
+			if (list_length(and_args) > 1)
+				datum_elem = makeBoolExpr(AND_EXPR, and_args, -1);
+
+			if (datum_elem)
+				datum_elems = lappend(datum_elems, datum_elem);
 		}
 	}
 
-	if (elems)
-	{
-		/*
-		 * Generate the operator expression from the non-null partition
-		 * values.
-		 */
-		opexpr = make_partition_op_expr(key, 0, BTEqualStrategyNumber,
-										keyCol, (Expr *) elems);
-	}
-	else
+	/*
+	 * Gin up a "col IS NOT NULL" test for every column that was not found to
+	 * have a NULL value assigned to it.  The test will be ANDed with the
+	 * other tests. This might seem redundant, but the partition routing
+	 * machinery needs it.
+	 */
+	for (i = 0; i < key->partnatts; i++)
 	{
-		/*
-		 * If there are no partition values, we don't need an operator
-		 * expression.
-		 */
-		opexpr = NULL;
+		if (!key_is_null[i])
+		{
+			NullTest   *notnull_test = NULL;
+
+			notnull_test = makeNode(NullTest);
+			notnull_test->arg = keyCol[i];
+			notnull_test->nulltesttype = IS_NOT_NULL;
+			notnull_test->argisrow = false;
+			notnull_test->location = -1;
+			result = lappend(result, notnull_test);
+		}
 	}
 
-	if (!list_has_null)
+	/*
+	 * Create an expression that ORs the results of per-list-bound
+	 * expressions.  For the single column case, make_partition_op_expr()
+	 * contains the logic to optionally use a ScalarArrayOpExpr, so
+	 * we use that.  XXX fix make_partition_op_expr() to handle the
+	 * multi-column case.
+	 */
+	if (datum_elems)
 	{
-		/*
-		 * Gin up a "col IS NOT NULL" test that will be ANDed with the main
-		 * expression.  This might seem redundant, but the partition routing
-		 * machinery needs it.
-		 */
-		nulltest = makeNode(NullTest);
-		nulltest->arg = keyCol;
-		nulltest->nulltesttype = IS_NOT_NULL;
-		nulltest->argisrow = false;
-		nulltest->location = -1;
-
-		result = opexpr ? list_make2(nulltest, opexpr) : list_make1(nulltest);
+		if (key->partnatts > 1)
+			datumtest = makeBoolExpr(OR_EXPR, datum_elems, -1);
+		else
+			datumtest = make_partition_op_expr(key, 0,
+											   BTEqualStrategyNumber,
+											   keyCol[0],
+											   (Expr *) datum_elems);
 	}
 	else
-	{
-		/*
-		 * Gin up a "col IS NULL" test that will be OR'd with the main
-		 * expression.
-		 */
-		nulltest = makeNode(NullTest);
-		nulltest->arg = keyCol;
-		nulltest->nulltesttype = IS_NULL;
-		nulltest->argisrow = false;
-		nulltest->location = -1;
+		datumtest = NULL;
 
-		if (opexpr)
-		{
-			Expr	   *or;
+	/*
+	 * is_null_test might have been set in the single-column case if
+	 * NULL is allowed, which OR with the datum expression if any.
+	 */
+	if (is_null_test && datumtest)
+	{
+		Expr *orexpr = makeBoolExpr(OR_EXPR,
+									list_make2(is_null_test, datumtest),
+									-1);
 
-			or = makeBoolExpr(OR_EXPR, list_make2(nulltest, opexpr), -1);
-			result = list_make1(or);
-		}
-		else
-			result = list_make1(nulltest);
+		result = lappend(result, orexpr);
 	}
+	else if (is_null_test)
+		result = lappend(result, is_null_test);
+	else if (datumtest)
+		result = lappend(result, datumtest);
 
 	/*
 	 * Note that, in general, applying NOT to a constraint expression doesn't
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index e00edbe..c7cd0b7 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -69,6 +69,8 @@ typedef struct PartClauseInfo
 	Oid			cmpfn;			/* Oid of function to compare 'expr' to the
 								 * partition key */
 	int			op_strategy;	/* btree strategy identifying the operator */
+	bool		is_null;		/* TRUE if clause contains NULL condition in case
+								   of list partitioning, FALSE otherwise */
 } PartClauseInfo;
 
 /*
@@ -134,7 +136,6 @@ typedef struct PruneStepResult
 	Bitmapset  *bound_offsets;
 
 	bool		scan_default;	/* Scan the default partition? */
-	bool		scan_null;		/* Scan the partition for NULL values? */
 } PruneStepResult;
 
 
@@ -185,8 +186,8 @@ static PruneStepResult *get_matching_hash_bounds(PartitionPruneContext *context,
 												 StrategyNumber opstrategy, Datum *values, int nvalues,
 												 FmgrInfo *partsupfunc, Bitmapset *nullkeys);
 static PruneStepResult *get_matching_list_bounds(PartitionPruneContext *context,
-												 StrategyNumber opstrategy, Datum value, int nvalues,
-												 FmgrInfo *partsupfunc, Bitmapset *nullkeys);
+												 StrategyNumber opstrategy, Datum *values, bool *isnulls,
+												 int nvalues, FmgrInfo *partsupfunc, Bitmapset *nullkeys);
 static PruneStepResult *get_matching_range_bounds(PartitionPruneContext *context,
 												  StrategyNumber opstrategy, Datum *values, int nvalues,
 												  FmgrInfo *partsupfunc, Bitmapset *nullkeys);
@@ -903,13 +904,6 @@ get_matching_partitions(PartitionPruneContext *context, List *pruning_steps)
 		result = bms_add_member(result, partindex);
 	}
 
-	/* Add the null and/or default partition if needed and present. */
-	if (final_result->scan_null)
-	{
-		Assert(context->strategy == PARTITION_STRATEGY_LIST);
-		Assert(partition_bound_accepts_nulls(context->boundinfo));
-		result = bms_add_member(result, context->boundinfo->null_index);
-	}
 	if (scan_default)
 	{
 		Assert(context->strategy == PARTITION_STRATEGY_LIST ||
@@ -1229,14 +1223,9 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 	 * Now generate some (more) pruning steps.  We have three strategies:
 	 *
 	 * 1) Generate pruning steps based on IS NULL clauses:
-	 *   a) For list partitioning, null partition keys can only be found in
-	 *      the designated null-accepting partition, so if there are IS NULL
-	 *      clauses containing partition keys we should generate a pruning
-	 *      step that gets rid of all partitions but that one.  We can
-	 *      disregard any OpExpr we may have found.
-	 *   b) For range partitioning, only the default partition can contain
+	 *   a) For range partitioning, only the default partition can contain
 	 *      NULL values, so the same rationale applies.
-	 *   c) For hash partitioning, we only apply this strategy if we have
+	 *   b) For hash partitioning, we only apply this strategy if we have
 	 *      IS NULL clauses for all the keys.  Strategy 2 below will take
 	 *      care of the case where some keys have OpExprs and others have
 	 *      IS NULL clauses.
@@ -1248,8 +1237,7 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 	 *    IS NOT NULL clauses for all partition keys.
 	 */
 	if (!bms_is_empty(nullkeys) &&
-		(part_scheme->strategy == PARTITION_STRATEGY_LIST ||
-		 part_scheme->strategy == PARTITION_STRATEGY_RANGE ||
+		(part_scheme->strategy == PARTITION_STRATEGY_RANGE ||
 		 (part_scheme->strategy == PARTITION_STRATEGY_HASH &&
 		  bms_num_members(nullkeys) == part_scheme->partnatts)))
 	{
@@ -1399,10 +1387,12 @@ gen_prune_steps_from_opexps(GeneratePruningStepsContext *context,
 		bool		consider_next_key = true;
 
 		/*
-		 * For range partitioning, if we have no clauses for the current key,
-		 * we can't consider any later keys either, so we can stop here.
+		 * For range partitioning and list partitioning, if we have no clauses
+		 * for the current key, we can't consider any later keys either, so we
+		 * can stop here.
 		 */
-		if (part_scheme->strategy == PARTITION_STRATEGY_RANGE &&
+		if ((part_scheme->strategy == PARTITION_STRATEGY_RANGE ||
+			 part_scheme->strategy == PARTITION_STRATEGY_LIST) &&
 			clauselist == NIL)
 			break;
 
@@ -1422,7 +1412,15 @@ gen_prune_steps_from_opexps(GeneratePruningStepsContext *context,
 						righttype;
 
 			/* Look up the operator's btree/hash strategy number. */
-			if (pc->op_strategy == InvalidStrategy)
+			if (pc->op_strategy == InvalidStrategy && pc->is_null)
+			{
+				/*
+				 * When the clause contains 'IS NULL' or 'IS NOT NULL' in case of
+				 * list partitioning, forcibly set the strategy to BTEqualStrategyNumber.
+				 */
+				pc->op_strategy = BTEqualStrategyNumber;
+			}
+			else if (pc->op_strategy == InvalidStrategy)
 				get_op_opfamily_properties(pc->opno,
 										   part_scheme->partopfamily[i],
 										   false,
@@ -2324,9 +2322,36 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 		if (!equal(arg, partkey))
 			return PARTCLAUSE_NOMATCH;
 
-		*clause_is_not_null = (nulltest->nulltesttype == IS_NOT_NULL);
+		if (part_scheme->strategy != PARTITION_STRATEGY_LIST)
+		{
+			*clause_is_not_null = (nulltest->nulltesttype == IS_NOT_NULL);
+			return PARTCLAUSE_MATCH_NULLNESS;
+		}
+		else
+		{
+			Const	*expr = makeConst(UNKNOWNOID, -1, InvalidOid, -2,
+									  (Datum) 0, true, false);
+			PartClauseInfo *partclause =
+				(PartClauseInfo *) palloc(sizeof(PartClauseInfo));
+
+			partclause->keyno = partkeyidx;
+			partclause->expr = (Expr *) expr;
+			partclause->is_null = true;
+
+			if (nulltest->nulltesttype == IS_NOT_NULL)
+			{
+				partclause->op_is_ne = true;
+				partclause->op_strategy = InvalidStrategy;
+			}
+			else
+			{
+				partclause->op_is_ne = false;
+				partclause->op_strategy = BTEqualStrategyNumber;
+			}
 
-		return PARTCLAUSE_MATCH_NULLNESS;
+			*pc = partclause;
+			return PARTCLAUSE_MATCH_CLAUSE;
+		}
 	}
 
 	/*
@@ -2627,13 +2652,170 @@ get_matching_hash_bounds(PartitionPruneContext *context,
 											  boundinfo->nindexes - 1);
 	}
 
+	return result;
+}
+
+/*
+ * get_min_and_max_off
+ *
+ * Fetches the minimum and maximum offset of the matching partitions.
+ */
+static void
+get_min_and_max_off(PartitionPruneContext *context, FmgrInfo *partsupfunc,
+					Datum *values, bool *isnulls, int nvalues, int off,
+					int *minoff, int *maxoff)
+{
+	PartitionBoundInfo	boundinfo = context->boundinfo;
+	Oid				   *partcollation = context->partcollation;
+	int					saved_off = off;
+
+	/* Find greatest bound that's smaller than the lookup value. */
+	while (off >= 1)
+	{
+		int32	cmpval =  partition_lbound_datum_cmp(partsupfunc, partcollation,
+													 boundinfo->datums[off - 1],
+													 boundinfo->isnulls[off - 1],
+													 values, isnulls, nvalues);
+
+		if (cmpval != 0)
+			break;
+
+		off--;
+	}
+
+	Assert(0 == partition_lbound_datum_cmp(partsupfunc, partcollation,
+										   boundinfo->datums[off],
+										   boundinfo->isnulls[off],
+										   values, isnulls, nvalues));
+
+	*minoff = off;
+
+	/* Find smallest bound that's greater than the lookup value. */
+	off = saved_off;
+	while (off < boundinfo->ndatums - 1)
+	{
+		int32	cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+													boundinfo->datums[off + 1],
+													boundinfo->isnulls[off + 1],
+													values, isnulls, nvalues);
+
+		if (cmpval != 0)
+			break;
+
+		off++;
+	}
+
+	Assert(0 == partition_lbound_datum_cmp(partsupfunc, partcollation,
+										   boundinfo->datums[off],
+										   boundinfo->isnulls[off],
+										   values, isnulls, nvalues));
+
+	*maxoff = off;
+	Assert(*minoff >= 0 && *maxoff >= 0);
+}
+
+/*
+ * get_min_or_max_off
+ *
+ * Fetches either minimum or maximum offset of the matching partitions
+ * depending on the value of is_min parameter.
+ */
+static int
+get_min_or_max_off(PartitionPruneContext *context, FmgrInfo *partsupfunc,
+				   Datum *values, bool *isnulls, int nvalues, int partnatts,
+				   bool is_equal, bool inclusive, int off, bool is_min)
+{
+	PartitionBoundInfo  boundinfo = context->boundinfo;
+	Oid                *partcollation = context->partcollation;
+
 	/*
-	 * There is neither a special hash null partition or the default hash
-	 * partition.
+	 * Based on whether the lookup values are minimum offset or maximum
+	 * offset (is_min indicates that) and whether they are inclusive or
+	 * not, we must either include the indexes of all such bounds in the
+	 * result (that is, return off to the index of smallest/greatest such
+	 * bound) or find the smallest/greatest one that's greater/smaller than
+	 * the lookup values and return the off.
 	 */
-	result->scan_null = result->scan_default = false;
+	if (off >= 0)
+	{
+		if (is_equal && nvalues < partnatts)
+		{
+			while (off >= 1 && off < boundinfo->ndatums - 1)
+			{
+				int32       cmpval;
+				int         nextoff;
 
-	return result;
+				if (is_min)
+					nextoff = inclusive ? off - 1 : off + 1;
+				else
+					nextoff = inclusive ? off + 1 : off - 1;
+
+				cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+													boundinfo->datums[nextoff],
+													boundinfo->isnulls[nextoff],
+													values, isnulls, nvalues);
+
+				if (cmpval != 0)
+					break;
+
+				off = nextoff;
+			}
+
+			Assert(0 == partition_lbound_datum_cmp(partsupfunc, partcollation,
+												   boundinfo->datums[off],
+												   boundinfo->isnulls[off],
+												   values, isnulls, nvalues));
+			if (is_min)
+				off = inclusive ? off : off + 1;
+			else
+				off = inclusive ? off + 1 : off;
+		}
+		else if (!is_equal || (is_min && !inclusive) || (!is_min && inclusive))
+			off = off + 1;
+		else
+			off = off;
+	}
+	else
+	{
+		if (is_min)
+			off = 0;
+		else
+			off = off + 1;
+	}
+
+	return off;
+}
+
+/*
+ * add_partitions
+ *
+ * Adds the non null partitions between minimum and maximum offset passed as
+ * input.
+ */
+static void
+add_partitions(PruneStepResult *result, bool **isnulls, int minoff, int maxoff,
+			   int ncols)
+{
+	int i;
+
+	Assert(minoff >= 0 && maxoff >= 0 && ncols > 0);
+	for (i = minoff; i < maxoff; i++)
+	{
+		int		j;
+		bool    isadd = true;
+
+		for (j = 0; j < ncols; j++)
+		{
+			if (isnulls[i][j])
+			{
+				isadd = false;
+				break;
+			}
+		}
+
+		if (isadd)
+			result->bound_offsets = bms_add_member(result->bound_offsets, i);
+	}
 }
 
 /*
@@ -2642,8 +2824,7 @@ get_matching_hash_bounds(PartitionPruneContext *context,
  *		according to the semantics of the given operator strategy
  *
  * scan_default will be set in the returned struct, if the default partition
- * needs to be scanned, provided one exists at all.  scan_null will be set if
- * the special null-accepting partition needs to be scanned.
+ * needs to be scanned, provided one exists at all.
  *
  * 'opstrategy' if non-zero must be a btree strategy number.
  *
@@ -2658,8 +2839,8 @@ get_matching_hash_bounds(PartitionPruneContext *context,
  */
 static PruneStepResult *
 get_matching_list_bounds(PartitionPruneContext *context,
-						 StrategyNumber opstrategy, Datum value, int nvalues,
-						 FmgrInfo *partsupfunc, Bitmapset *nullkeys)
+						 StrategyNumber opstrategy, Datum *values, bool *isnulls,
+						 int nvalues, FmgrInfo *partsupfunc, Bitmapset *nullkeys)
 {
 	PruneStepResult *result = (PruneStepResult *) palloc0(sizeof(PruneStepResult));
 	PartitionBoundInfo boundinfo = context->boundinfo;
@@ -2669,25 +2850,9 @@ get_matching_list_bounds(PartitionPruneContext *context,
 	bool		is_equal;
 	bool		inclusive = false;
 	Oid		   *partcollation = context->partcollation;
+	int         partnatts = context->partnatts;
 
 	Assert(context->strategy == PARTITION_STRATEGY_LIST);
-	Assert(context->partnatts == 1);
-
-	result->scan_null = result->scan_default = false;
-
-	if (!bms_is_empty(nullkeys))
-	{
-		/*
-		 * Nulls may exist in only one partition - the partition whose
-		 * accepted set of values includes null or the default partition if
-		 * the former doesn't exist.
-		 */
-		if (partition_bound_accepts_nulls(boundinfo))
-			result->scan_null = true;
-		else
-			result->scan_default = partition_bound_has_default(boundinfo);
-		return result;
-	}
 
 	/*
 	 * If there are no datums to compare keys with, but there are partitions,
@@ -2700,7 +2865,7 @@ get_matching_list_bounds(PartitionPruneContext *context,
 	}
 
 	minoff = 0;
-	maxoff = boundinfo->ndatums - 1;
+	maxoff = boundinfo->ndatums;
 
 	/*
 	 * If there are no values to compare with the datums in boundinfo, it
@@ -2709,10 +2874,10 @@ get_matching_list_bounds(PartitionPruneContext *context,
 	 */
 	if (nvalues == 0)
 	{
-		Assert(boundinfo->ndatums > 0);
-		result->bound_offsets = bms_add_range(NULL, 0,
-											  boundinfo->ndatums - 1);
+		add_partitions(result, boundinfo->isnulls, 0, boundinfo->ndatums,
+					   context->partnatts);
 		result->scan_default = partition_bound_has_default(boundinfo);
+
 		return result;
 	}
 
@@ -2722,19 +2887,36 @@ get_matching_list_bounds(PartitionPruneContext *context,
 		/*
 		 * First match to all bounds.  We'll remove any matching datums below.
 		 */
-		Assert(boundinfo->ndatums > 0);
-		result->bound_offsets = bms_add_range(NULL, 0,
-											  boundinfo->ndatums - 1);
+		add_partitions(result, boundinfo->isnulls, 0, boundinfo->ndatums,
+					   nvalues);
 
 		off = partition_list_bsearch(partsupfunc, partcollation, boundinfo,
-									 value, &is_equal);
+									 values, isnulls, nvalues, &is_equal);
 		if (off >= 0 && is_equal)
 		{
+			if (nvalues == partnatts)
+			{
+				/* We have a match. Remove from the result. */
+				Assert(boundinfo->indexes[off] >= 0);
+				result->bound_offsets = bms_del_member(result->bound_offsets, off);
+			}
+			else
+			{
+				int i;
 
-			/* We have a match. Remove from the result. */
-			Assert(boundinfo->indexes[off] >= 0);
-			result->bound_offsets = bms_del_member(result->bound_offsets,
-												   off);
+				/*
+				 * Since the lookup value contains only a prefix of keys,
+				 * we must find other bounds that may also match the prefix.
+				 * partition_list_bsearch() returns the offset of one of them,
+				 * find others by checking adjacent bounds.
+				 */
+				get_min_and_max_off(context, partsupfunc, values, isnulls,
+									nvalues, off, &minoff, &maxoff);
+
+				/* Remove all matching bounds from the result. */
+				for (i = minoff; i <= maxoff; i++)
+					result->bound_offsets = bms_del_member(result->bound_offsets, i);
+			}
 		}
 
 		/* Always include the default partition if any. */
@@ -2757,41 +2939,53 @@ get_matching_list_bounds(PartitionPruneContext *context,
 	switch (opstrategy)
 	{
 		case BTEqualStrategyNumber:
-			off = partition_list_bsearch(partsupfunc,
-										 partcollation,
-										 boundinfo, value,
-										 &is_equal);
+			off = partition_list_bsearch(partsupfunc, partcollation, boundinfo,
+										 values, isnulls, nvalues, &is_equal);
+
 			if (off >= 0 && is_equal)
 			{
-				Assert(boundinfo->indexes[off] >= 0);
-				result->bound_offsets = bms_make_singleton(off);
+				if (nvalues == partnatts)
+				{
+					/* We have a match. Add to the result. */
+					Assert(boundinfo->indexes[off] >= 0);
+					result->bound_offsets = bms_make_singleton(off);
+					return result;
+				}
+				else
+				{
+					/*
+					 * Since the lookup value contains only a prefix of keys,
+					 * we must find other bounds that may also match the prefix.
+					 * partition_list_bsearch() returns the offset of one of them,
+					 * find others by checking adjacent bounds.
+					 */
+					get_min_and_max_off(context, partsupfunc, values, isnulls,
+										nvalues, off, &minoff, &maxoff);
+
+					/* Add all matching bounds to the result. */
+					result->bound_offsets = bms_add_range(NULL, minoff, maxoff);
+				}
 			}
 			else
 				result->scan_default = partition_bound_has_default(boundinfo);
+
 			return result;
 
 		case BTGreaterEqualStrategyNumber:
 			inclusive = true;
 			/* fall through */
 		case BTGreaterStrategyNumber:
-			off = partition_list_bsearch(partsupfunc,
-										 partcollation,
-										 boundinfo, value,
-										 &is_equal);
-			if (off >= 0)
-			{
-				/* We don't want the matched datum to be in the result. */
-				if (!is_equal || !inclusive)
-					off++;
-			}
-			else
-			{
-				/*
-				 * This case means all partition bounds are greater, which in
-				 * turn means that all partitions satisfy this key.
-				 */
-				off = 0;
-			}
+			off = partition_list_bsearch(partsupfunc, partcollation, boundinfo,
+										 values, isnulls, nvalues, &is_equal);
+
+			/*
+			 * Since the lookup value contains only a prefix of keys,
+			 * we must find other bounds that may also match the prefix.
+			 * partition_list_bsearch returns the offset of one of them,
+			 * find others by checking adjacent bounds.
+			 */
+			off = get_min_or_max_off(context, partsupfunc, values, isnulls, nvalues,
+									 partnatts, is_equal, inclusive, off, true);
 
 			/*
 			 * off is greater than the numbers of datums we have partitions
@@ -2809,12 +3003,17 @@ get_matching_list_bounds(PartitionPruneContext *context,
 			inclusive = true;
 			/* fall through */
 		case BTLessStrategyNumber:
-			off = partition_list_bsearch(partsupfunc,
-										 partcollation,
-										 boundinfo, value,
-										 &is_equal);
-			if (off >= 0 && is_equal && !inclusive)
-				off--;
+			off = partition_list_bsearch(partsupfunc, partcollation, boundinfo,
+										 values, isnulls, nvalues, &is_equal);
+
+			/*
+			 * Since the lookup value contains only a prefix of keys,
+			 * we must find other bounds that may also match the prefix.
+			 * partition_list_bsearch returns the offset of one of them,
+			 * find others by checking adjacent bounds.
+			 */
+			off = get_min_or_max_off(context, partsupfunc, values, isnulls, nvalues,
+									 partnatts, is_equal, inclusive, off, false);
 
 			/*
 			 * off is smaller than the datums of all non-default partitions.
@@ -2833,8 +3032,7 @@ get_matching_list_bounds(PartitionPruneContext *context,
 			break;
 	}
 
-	Assert(minoff >= 0 && maxoff >= 0);
-	result->bound_offsets = bms_add_range(NULL, minoff, maxoff);
+	add_partitions(result, boundinfo->isnulls, minoff, maxoff, nvalues);
 	return result;
 }
 
@@ -2886,8 +3084,6 @@ get_matching_range_bounds(PartitionPruneContext *context,
 	Assert(context->strategy == PARTITION_STRATEGY_RANGE);
 	Assert(nvalues <= partnatts);
 
-	result->scan_null = result->scan_default = false;
-
 	/*
 	 * If there are no datums to compare keys with, or if we got an IS NULL
 	 * clause just return the default partition, if it exists.
@@ -3343,6 +3539,7 @@ perform_pruning_base_step(PartitionPruneContext *context,
 	Datum		values[PARTITION_MAX_KEYS];
 	FmgrInfo   *partsupfunc;
 	int			stateidx;
+	bool		isnulls[PARTITION_MAX_KEYS];
 
 	/*
 	 * There better be the same number of expressions and compare functions.
@@ -3364,14 +3561,16 @@ perform_pruning_base_step(PartitionPruneContext *context,
 		 * not provided in operator clauses, but instead the planner found
 		 * that they appeared in a IS NULL clause.
 		 */
-		if (bms_is_member(keyno, opstep->nullkeys))
+		if (bms_is_member(keyno, opstep->nullkeys) &&
+			context->strategy != PARTITION_STRATEGY_LIST)
 			continue;
 
 		/*
-		 * For range partitioning, we must only perform pruning with values
-		 * for either all partition keys or a prefix thereof.
+		 * For range partitioning and list partitioning, we must only perform
+		 * pruning with values for either all partition keys or a prefix thereof.
 		 */
-		if (keyno > nvalues && context->strategy == PARTITION_STRATEGY_RANGE)
+		if (keyno > nvalues && (context->strategy == PARTITION_STRATEGY_RANGE ||
+								context->strategy == PARTITION_STRATEGY_LIST))
 			break;
 
 		if (lc1 != NULL)
@@ -3389,42 +3588,51 @@ perform_pruning_base_step(PartitionPruneContext *context,
 
 			/*
 			 * Since we only allow strict operators in pruning steps, any
-			 * null-valued comparison value must cause the comparison to fail,
-			 * so that no partitions could match.
+			 * null-valued comparison value must cause the comparison to fail
+			 * in cases other than list partitioning, so that no partitions could
+			 * match.
 			 */
-			if (isnull)
+			if (isnull && context->strategy != PARTITION_STRATEGY_LIST)
 			{
 				PruneStepResult *result;
 
 				result = (PruneStepResult *) palloc(sizeof(PruneStepResult));
 				result->bound_offsets = NULL;
 				result->scan_default = false;
-				result->scan_null = false;
 
 				return result;
 			}
 
 			/* Set up the stepcmpfuncs entry, unless we already did */
-			cmpfn = lfirst_oid(lc2);
-			Assert(OidIsValid(cmpfn));
-			if (cmpfn != context->stepcmpfuncs[stateidx].fn_oid)
+			if (!isnull)
 			{
-				/*
-				 * If the needed support function is the same one cached in
-				 * the relation's partition key, copy the cached FmgrInfo.
-				 * Otherwise (i.e., when we have a cross-type comparison), an
-				 * actual lookup is required.
-				 */
-				if (cmpfn == context->partsupfunc[keyno].fn_oid)
-					fmgr_info_copy(&context->stepcmpfuncs[stateidx],
-								   &context->partsupfunc[keyno],
-								   context->ppccontext);
-				else
-					fmgr_info_cxt(cmpfn, &context->stepcmpfuncs[stateidx],
-								  context->ppccontext);
-			}
+				cmpfn = lfirst_oid(lc2);
+				Assert(OidIsValid(cmpfn));
+				if (cmpfn != context->stepcmpfuncs[stateidx].fn_oid)
+				{
+					/*
+					 * If the needed support function is the same one cached in
+					 * the relation's partition key, copy the cached FmgrInfo.
+					 * Otherwise (i.e., when we have a cross-type comparison), an
+					 * actual lookup is required.
+					 */
+					if (cmpfn == context->partsupfunc[keyno].fn_oid)
+						fmgr_info_copy(&context->stepcmpfuncs[stateidx],
+									   &context->partsupfunc[keyno],
+									   context->ppccontext);
+					else
+						fmgr_info_cxt(cmpfn, &context->stepcmpfuncs[stateidx],
+									  context->ppccontext);
+				}
 
-			values[keyno] = datum;
+				values[keyno] = datum;
+				isnulls[keyno] = false;
+			}
+			else
+			{
+				values[keyno] = (Datum) 0;
+				isnulls[keyno] = true;
+			}
 			nvalues++;
 
 			lc1 = lnext(opstep->exprs, lc1);
@@ -3451,7 +3659,7 @@ perform_pruning_base_step(PartitionPruneContext *context,
 		case PARTITION_STRATEGY_LIST:
 			return get_matching_list_bounds(context,
 											opstep->opstrategy,
-											values[0], nvalues,
+											values, isnulls, nvalues,
 											&partsupfunc[0],
 											opstep->nullkeys);
 
@@ -3500,7 +3708,6 @@ perform_pruning_combine_step(PartitionPruneContext *context,
 		result->bound_offsets =
 			bms_add_range(NULL, 0, boundinfo->nindexes - 1);
 		result->scan_default = partition_bound_has_default(boundinfo);
-		result->scan_null = partition_bound_accepts_nulls(boundinfo);
 		return result;
 	}
 
@@ -3527,9 +3734,7 @@ perform_pruning_combine_step(PartitionPruneContext *context,
 				result->bound_offsets = bms_add_members(result->bound_offsets,
 														step_result->bound_offsets);
 
-				/* Update whether to scan null and default partitions. */
-				if (!result->scan_null)
-					result->scan_null = step_result->scan_null;
+				/* Update whether to scan default partitions. */
 				if (!result->scan_default)
 					result->scan_default = step_result->scan_default;
 			}
@@ -3552,7 +3757,6 @@ perform_pruning_combine_step(PartitionPruneContext *context,
 					/* Copy step's result the first time. */
 					result->bound_offsets =
 						bms_copy(step_result->bound_offsets);
-					result->scan_null = step_result->scan_null;
 					result->scan_default = step_result->scan_default;
 					firststep = false;
 				}
@@ -3563,9 +3767,7 @@ perform_pruning_combine_step(PartitionPruneContext *context,
 						bms_int_members(result->bound_offsets,
 										step_result->bound_offsets);
 
-					/* Update whether to scan null and default partitions. */
-					if (result->scan_null)
-						result->scan_null = step_result->scan_null;
+					/* Update whether to scan default partitions. */
 					if (result->scan_default)
 						result->scan_default = step_result->scan_default;
 				}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 1bb2573..a449490 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9449,10 +9449,9 @@ get_rule_expr(Node *node, deparse_context *context,
 						sep = "";
 						foreach(cell, spec->listdatums)
 						{
-							Const	   *val = lfirst_node(Const, cell);
-
 							appendStringInfoString(buf, sep);
-							get_const_expr(val, context, -1);
+							appendStringInfoString
+								(buf, get_list_partbound_value_string(lfirst(cell)));
 							sep = ", ";
 						}
 
@@ -12013,6 +12012,46 @@ flatten_reloptions(Oid relid)
 }
 
 /*
+ * get_list_partbound_value_string
+ *
+ * A C string representation of one list partition bound value
+ */
+char *
+get_list_partbound_value_string(List *bound_value)
+{
+	StringInfo  	buf = makeStringInfo();
+	StringInfo  	boundconstraint = makeStringInfo();
+	deparse_context context;
+	ListCell	   *cell;
+	char		   *sep = "";
+	int				ncols = 0;
+
+	memset(&context, 0, sizeof(deparse_context));
+	context.buf = buf;
+
+	foreach(cell, bound_value)
+	{
+		Const      *val = castNode(Const, lfirst(cell));
+
+		appendStringInfoString(buf, sep);
+		get_const_expr(val, &context, -1);
+		sep = ", ";
+		ncols++;
+	}
+
+	if (ncols > 1)
+	{
+		appendStringInfoChar(boundconstraint, '(');
+		appendStringInfoString(boundconstraint, buf->data);
+		appendStringInfoChar(boundconstraint, ')');
+
+		return boundconstraint->data;
+	}
+	else
+		return buf->data;
+}
+
+/*
  * get_range_partbound_string
  *		A C string representation of one range partition bound
  */
diff --git a/src/include/partitioning/partbounds.h b/src/include/partitioning/partbounds.h
index 7138cb1..4afedce 100644
--- a/src/include/partitioning/partbounds.h
+++ b/src/include/partitioning/partbounds.h
@@ -24,9 +24,6 @@ struct RelOptInfo;				/* avoid including pathnodes.h here */
  * descriptor, but may also be used to represent a virtual partitioned
  * table such as a partitioned joinrel within the planner.
  *
- * A list partition datum that is known to be NULL is never put into the
- * datums array. Instead, it is tracked using the null_index field.
- *
  * In the case of range partitioning, ndatums will typically be far less than
  * 2 * nparts, because a partition's upper bound and the next partition's lower
  * bound are the same in most common cases, and we only store one of them (the
@@ -38,6 +35,10 @@ struct RelOptInfo;				/* avoid including pathnodes.h here */
  * of datum-tuples with 2 datums, modulus and remainder, corresponding to a
  * given partition.
  *
+ * isnulls is an array of boolean-tuples with key->partnatts boolean values
+ * each.  Currently only used for list partitioning, it stores whether a
+ * given partition key accepts NULL as value.
+ *
  * The datums in datums array are arranged in increasing order as defined by
  * functions qsort_partition_rbound_cmp(), qsort_partition_list_value_cmp() and
  * qsort_partition_hbound_cmp() for range, list and hash partitioned tables
@@ -79,8 +80,10 @@ struct RelOptInfo;				/* avoid including pathnodes.h here */
 typedef struct PartitionBoundInfoData
 {
 	char		strategy;		/* hash, list or range? */
+	int			partnatts;		/* number of partition key columns */
 	int			ndatums;		/* Length of the datums[] array */
 	Datum	  **datums;
+	bool	  **isnulls;
 	PartitionRangeDatumKind **kind; /* The kind of each range bound datum;
 									 * NULL for hash and list partitioned
 									 * tables */
@@ -89,15 +92,14 @@ typedef struct PartitionBoundInfoData
 									 * only set for LIST partitioned tables */
 	int			nindexes;		/* Length of the indexes[] array */
 	int		   *indexes;		/* Partition indexes */
-	int			null_index;		/* Index of the null-accepting partition; -1
-								 * if there isn't one */
 	int			default_index;	/* Index of the default partition; -1 if there
 								 * isn't one */
 } PartitionBoundInfoData;
 
-#define partition_bound_accepts_nulls(bi) ((bi)->null_index != -1)
 #define partition_bound_has_default(bi) ((bi)->default_index != -1)
 
+extern bool partition_bound_accepts_nulls(PartitionBoundInfo boundinfo);
+
 extern int	get_hash_partition_greatest_modulus(PartitionBoundInfo b);
 extern uint64 compute_partition_hash_value(int partnatts, FmgrInfo *partsupfunc,
 										   Oid *partcollation,
@@ -132,10 +134,15 @@ extern int32 partition_rbound_datum_cmp(FmgrInfo *partsupfunc,
 										Oid *partcollation,
 										Datum *rb_datums, PartitionRangeDatumKind *rb_kind,
 										Datum *tuple_datums, int n_tuple_datums);
+extern int32 partition_lbound_datum_cmp(FmgrInfo *partsupfunc,
+										Oid *partcollation,
+										Datum *lb_datums, bool *lb_isnulls,
+										Datum *values, bool *isnulls, int nvalues);
 extern int	partition_list_bsearch(FmgrInfo *partsupfunc,
 								   Oid *partcollation,
 								   PartitionBoundInfo boundinfo,
-								   Datum value, bool *is_equal);
+								   Datum *values, bool *isnulls,
+								   int nvalues, bool *is_equal);
 extern int	partition_range_datum_bsearch(FmgrInfo *partsupfunc,
 										  Oid *partcollation,
 										  PartitionBoundInfo boundinfo,
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index d333e5e..60dac6d 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -40,6 +40,7 @@ extern List *select_rtable_names_for_explain(List *rtable,
 extern char *generate_collation_name(Oid collid);
 extern char *generate_opclass_name(Oid opclass);
 extern char *get_range_partbound_string(List *bound_datums);
+extern char *get_list_partbound_value_string(List *bound_value);
 
 extern char *pg_get_statisticsobjdef_string(Oid statextid);
 
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index a958b84..cfc865e 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -352,12 +352,6 @@ CREATE TABLE partitioned (
 	a int
 ) INHERITS (some_table) PARTITION BY LIST (a);
 ERROR:  cannot create partitioned table as inheritance child
--- cannot use more than 1 column as partition key for list partitioned table
-CREATE TABLE partitioned (
-	a1 int,
-	a2 int
-) PARTITION BY LIST (a1, a2);	-- fail
-ERROR:  cannot use "list" partition strategy with more than one column
 -- unsupported constraint type for partitioned tables
 CREATE TABLE partitioned (
 	a int,
@@ -677,6 +671,11 @@ CREATE TABLE fail_default_part PARTITION OF list_parted DEFAULT;
 ERROR:  partition "fail_default_part" conflicts with existing default partition "part_default"
 LINE 1: ...TE TABLE fail_default_part PARTITION OF list_parted DEFAULT;
                                                                ^
+-- trying to specify more number of values than the number of partition keys
+CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN ((1, 2));
+ERROR:  Must specify exactly one value per partitioning column
+LINE 1: ...BLE fail_part PARTITION OF list_parted FOR VALUES IN ((1, 2)...
+                                                             ^
 -- specified literal can't be cast to the partition column data type
 CREATE TABLE bools (
 	a bool
@@ -919,6 +918,48 @@ CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, minvalue)
 ERROR:  partition "fail_part" would overlap partition "part10"
 LINE 1: ..._part PARTITION OF range_parted3 FOR VALUES FROM (1, minvalu...
                                                              ^
+-- now check for multi-column list partition key
+CREATE TABLE list_parted3 (
+	a int,
+	b varchar
+) PARTITION BY LIST (a, b);
+CREATE TABLE list_parted3_p1 PARTITION OF list_parted3 FOR VALUES IN ((1, 'A'));
+CREATE TABLE list_parted3_p2 PARTITION OF list_parted3 FOR VALUES IN ((1, 'B'),(1, 'E'), (1, 'E'), (2, 'C'),(2, 'D'));
+CREATE TABLE list_parted3_p3 PARTITION OF list_parted3 FOR VALUES IN ((1, NULL),(NULL, 'F'));
+CREATE TABLE list_parted3_p4 PARTITION OF list_parted3 FOR VALUES IN ((NULL, NULL));
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((1, 'E'));
+ERROR:  partition "fail_part" would overlap partition "list_parted3_p2"
+LINE 1: ...ail_part PARTITION OF list_parted3 FOR VALUES IN ((1, 'E'));
+                                                                 ^
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((1, NULL));
+ERROR:  partition "fail_part" would overlap partition "list_parted3_p3"
+LINE 1: ...il_part PARTITION OF list_parted3 FOR VALUES IN ((1, NULL));
+                                                                ^
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((NULL, 'F'));
+ERROR:  partition "fail_part" would overlap partition "list_parted3_p3"
+LINE 1: ..._part PARTITION OF list_parted3 FOR VALUES IN ((NULL, 'F'));
+                                                                 ^
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((NULL, NULL));
+ERROR:  partition "fail_part" would overlap partition "list_parted3_p4"
+LINE 1: ...part PARTITION OF list_parted3 FOR VALUES IN ((NULL, NULL));
+                                                                ^
+CREATE TABLE list_parted3_default PARTITION OF list_parted3 DEFAULT;
+-- trying to specify less number of values than the number of partition keys
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN (10, 'N');
+ERROR:  Invalid list bound specification
+LINE 1: ...LE fail_part PARTITION OF list_parted3 FOR VALUES IN (10, 'N...
+                                                             ^
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((10), ('N'));
+ERROR:  Invalid list bound specification
+LINE 1: ...LE fail_part PARTITION OF list_parted3 FOR VALUES IN ((10), ...
+                                                             ^
+-- trying to specify more number of values than the number of partition keys
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((10, 'N', 10));
+ERROR:  Must specify exactly one value per partitioning column
+LINE 1: ...LE fail_part PARTITION OF list_parted3 FOR VALUES IN ((10, '...
+                                                             ^
+-- cleanup
+DROP TABLE list_parted3;
 -- check for partition bound overlap and other invalid specifications for the hash partition
 CREATE TABLE hash_parted2 (
 	a varchar
diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out
index 5063a3d..038cc53 100644
--- a/src/test/regress/expected/insert.out
+++ b/src/test/regress/expected/insert.out
@@ -808,6 +808,63 @@ select tableoid::regclass::text, * from mcrparted order by 1;
 
 -- cleanup
 drop table mcrparted;
+-- Test multi-column list partitioning with 3 partition keys
+create table mclparted (a int, b text, c int) partition by list (a, b, c);
+create table mclparted_p1 partition of mclparted for values in ((1, 'a', 1));
+create table mclparted_p2 partition of mclparted for values in ((1, 'a', 2), (1, 'b', 1), (2, 'a', 1));
+create table mclparted_p3 partition of mclparted for values in ((3, 'c', 3), (4, 'd', 4), (5, 'e', 5), (6, null, 6));
+create table mclparted_p4 partition of mclparted for values in ((null, 'a', 1), (1, null, 1), (1, 'a', null));
+create table mclparted_p5 partition of mclparted for values in ((null, null, null));
+-- routed to mclparted_p1
+insert into mclparted values (1, 'a', 1);
+-- routed to mclparted_p2
+insert into mclparted values (1, 'a', 2);
+insert into mclparted values (1, 'b', 1);
+insert into mclparted values (2, 'a', 1);
+-- routed to mclparted_p3
+insert into mclparted values (3, 'c', 3);
+insert into mclparted values (4, 'd', 4);
+insert into mclparted values (5, 'e', 5);
+insert into mclparted values (6, null, 6);
+-- routed to mclparted_p4
+insert into mclparted values (null, 'a', 1);
+insert into mclparted values (1, null, 1);
+insert into mclparted values (1, 'a', null);
+-- routed to mclparted_p5
+insert into mclparted values (null, null, null);
+-- error cases
+insert into mclparted values (10, 'a', 1);
+ERROR:  no partition of relation "mclparted" found for row
+DETAIL:  Partition key of the failing row contains (a, b, c) = (10, a, 1).
+insert into mclparted values (1, 'z', 1);
+ERROR:  no partition of relation "mclparted" found for row
+DETAIL:  Partition key of the failing row contains (a, b, c) = (1, z, 1).
+insert into mclparted values (1, 'a', 10);
+ERROR:  no partition of relation "mclparted" found for row
+DETAIL:  Partition key of the failing row contains (a, b, c) = (1, a, 10).
+insert into mclparted values (1, null, null);
+ERROR:  no partition of relation "mclparted" found for row
+DETAIL:  Partition key of the failing row contains (a, b, c) = (1, null, null).
+-- check rows
+select tableoid::regclass::text, * from mclparted order by 1, 2, 3, 4;
+   tableoid   | a | b | c 
+--------------+---+---+---
+ mclparted_p1 | 1 | a | 1
+ mclparted_p2 | 1 | a | 2
+ mclparted_p2 | 1 | b | 1
+ mclparted_p2 | 2 | a | 1
+ mclparted_p3 | 3 | c | 3
+ mclparted_p3 | 4 | d | 4
+ mclparted_p3 | 5 | e | 5
+ mclparted_p3 | 6 |   | 6
+ mclparted_p4 | 1 | a |  
+ mclparted_p4 | 1 |   | 1
+ mclparted_p4 |   | a | 1
+ mclparted_p5 |   |   |  
+(12 rows)
+
+-- cleanup
+drop table mclparted;
 -- check that a BR constraint can't make partition contain violating rows
 create table brtrigpartcon (a int, b text) partition by list (a);
 create table brtrigpartcon1 partition of brtrigpartcon for values in (1);
@@ -981,6 +1038,96 @@ select tableoid::regclass, * from mcrparted order by a, b;
 (11 rows)
 
 drop table mcrparted;
+-- check multi-column list partitioning with partition key constraint
+create table mclparted (a text, b int) partition by list(a, b);
+create table mclparted_p1 partition of mclparted for values in (('a', 1));
+create table mclparted_p2 partition of mclparted for values in (('a', 2), ('b', 1), ('c', 3), ('d', 3), ('e', 3));
+create table mclparted_p3 partition of mclparted for values in (('a', 3), ('a', 4), ('a', null), (null, 1));
+create table mclparted_p4 partition of mclparted for values in (('b', null), (null, 2));
+create table mclparted_p5 partition of mclparted for values in ((null, null));
+create table mclparted_p6 partition of mclparted DEFAULT;
+\d+ mclparted
+                           Partitioned table "public.mclparted"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | text    |           |          |         | extended |              | 
+ b      | integer |           |          |         | plain    |              | 
+Partition key: LIST (a, b)
+Partitions: mclparted_p1 FOR VALUES IN (('a', 1)),
+            mclparted_p2 FOR VALUES IN (('a', 2), ('b', 1), ('c', 3), ('d', 3), ('e', 3)),
+            mclparted_p3 FOR VALUES IN (('a', 3), ('a', 4), ('a', NULL), (NULL, 1)),
+            mclparted_p4 FOR VALUES IN (('b', NULL), (NULL, 2)),
+            mclparted_p5 FOR VALUES IN ((NULL, NULL)),
+            mclparted_p6 DEFAULT
+
+\d+ mclparted_p1
+                                Table "public.mclparted_p1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | text    |           |          |         | extended |              | 
+ b      | integer |           |          |         | plain    |              | 
+Partition of: mclparted FOR VALUES IN (('a', 1))
+Partition constraint: ((a IS NOT NULL) AND (b IS NOT NULL) AND (((a = 'a'::text) AND (b = 1))))
+
+\d+ mclparted_p2
+                                Table "public.mclparted_p2"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | text    |           |          |         | extended |              | 
+ b      | integer |           |          |         | plain    |              | 
+Partition of: mclparted FOR VALUES IN (('a', 2), ('b', 1), ('c', 3), ('d', 3), ('e', 3))
+Partition constraint: ((a IS NOT NULL) AND (b IS NOT NULL) AND (((a = 'a'::text) AND (b = 2)) OR ((a = 'b'::text) AND (b = 1)) OR ((a = 'c'::text) AND (b = 3)) OR ((a = 'd'::text) AND (b = 3)) OR ((a = 'e'::text) AND (b = 3))))
+
+\d+ mclparted_p3
+                                Table "public.mclparted_p3"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | text    |           |          |         | extended |              | 
+ b      | integer |           |          |         | plain    |              | 
+Partition of: mclparted FOR VALUES IN (('a', 3), ('a', 4), ('a', NULL), (NULL, 1))
+Partition constraint: (((a = 'a'::text) AND (b = 3)) OR ((a = 'a'::text) AND (b = 4)) OR ((a = 'a'::text) AND (b IS NULL)) OR ((a IS NULL) AND (b = 1)))
+
+\d+ mclparted_p4
+                                Table "public.mclparted_p4"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | text    |           |          |         | extended |              | 
+ b      | integer |           |          |         | plain    |              | 
+Partition of: mclparted FOR VALUES IN (('b', NULL), (NULL, 2))
+Partition constraint: (((a = 'b'::text) AND (b IS NULL)) OR ((a IS NULL) AND (b = 2)))
+
+\d+ mclparted_p5
+                                Table "public.mclparted_p5"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | text    |           |          |         | extended |              | 
+ b      | integer |           |          |         | plain    |              | 
+Partition of: mclparted FOR VALUES IN ((NULL, NULL))
+Partition constraint: (((a IS NULL) AND (b IS NULL)))
+
+insert into mclparted values ('a', 1), ('a', 2), ('b', 1), ('c', 3), ('d', 3),
+	('e', 3), ('a', 3), ('a', 4), ('a', null), (null, 1), ('b', null),
+	(null, 2), (null, null), ('z', 10);
+select tableoid::regclass, * from mclparted order by a, b;
+   tableoid   | a | b  
+--------------+---+----
+ mclparted_p1 | a |  1
+ mclparted_p2 | a |  2
+ mclparted_p3 | a |  3
+ mclparted_p3 | a |  4
+ mclparted_p3 | a |   
+ mclparted_p2 | b |  1
+ mclparted_p4 | b |   
+ mclparted_p2 | c |  3
+ mclparted_p2 | d |  3
+ mclparted_p2 | e |  3
+ mclparted_p6 | z | 10
+ mclparted_p3 |   |  1
+ mclparted_p4 |   |  2
+ mclparted_p5 |   |   
+(14 rows)
+
+drop table mclparted;
 -- check that wholerow vars in the RETURNING list work with partitioned tables
 create table returningwrtest (a int) partition by list (a);
 create table returningwrtest1 partition of returningwrtest for values in (1);
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 27f7525..84b5b36 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -4650,6 +4650,1263 @@ SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM (plt1_adv t1 LEFT JOIN plt2_adv t
 DROP TABLE plt1_adv;
 DROP TABLE plt2_adv;
 DROP TABLE plt3_adv;
+-- Tests for multi-column list-partitioned tables
+CREATE TABLE plt1_adv_m (a int, b int, c text, d int) PARTITION BY LIST (c, d);
+CREATE TABLE plt1_adv_m_p1 PARTITION OF plt1_adv_m FOR VALUES IN (('0001', 1), ('0003', 3));
+CREATE TABLE plt1_adv_m_p2 PARTITION OF plt1_adv_m FOR VALUES IN (('0004', 4), ('0006', 6));
+CREATE TABLE plt1_adv_m_p3 PARTITION OF plt1_adv_m FOR VALUES IN (('0008', 8), ('0009', 9));
+INSERT INTO plt1_adv_m SELECT i, i, to_char(i % 10, 'FM0000'), (i % 10) FROM generate_series(1, 299) i WHERE i % 10 IN (1, 3, 4, 6, 8, 9);
+ANALYZE plt1_adv_m;
+CREATE TABLE plt2_adv_m (a int, b int, c text, d int) PARTITION BY LIST (c, d);
+CREATE TABLE plt2_adv_m_p1 PARTITION OF plt2_adv_m FOR VALUES IN (('0002', 2), ('0003', 3));
+CREATE TABLE plt2_adv_m_p2 PARTITION OF plt2_adv_m FOR VALUES IN (('0004', 4), ('0006', 6));
+CREATE TABLE plt2_adv_m_p3 PARTITION OF plt2_adv_m FOR VALUES IN (('0007', 7), ('0009', 9));
+INSERT INTO plt2_adv_m SELECT i, i, to_char(i % 10, 'FM0000'), (i % 10) FROM generate_series(1, 299) i WHERE i % 10 IN (2, 3, 4, 6, 7, 9);
+ANALYZE plt2_adv_m;
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Hash Join
+               Hash Cond: ((t2_1.a = t1_1.a) AND (t2_1.c = t1_1.c) AND (t2_1.d = t1_1.d))
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                           Filter: (a < 10)
+         ->  Hash Join
+               Hash Cond: ((t2_2.a = t1_2.a) AND (t2_2.c = t1_2.c) AND (t2_2.d = t1_2.d))
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (a < 10)
+         ->  Hash Join
+               Hash Cond: ((t2_3.a = t1_3.a) AND (t2_3.c = t1_3.c) AND (t2_3.d = t1_3.d))
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+                           Filter: (a < 10)
+(21 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+ a |  c   | d | a |  c   | d 
+---+------+---+---+------+---
+ 3 | 0003 | 3 | 3 | 0003 | 3
+ 4 | 0004 | 4 | 4 | 0004 | 4
+ 6 | 0006 | 6 | 6 | 0006 | 6
+ 9 | 0009 | 9 | 9 | 0009 | 9
+(4 rows)
+
+-- semi join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+                                         QUERY PLAN                                         
+--------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Nested Loop Semi Join
+               Join Filter: ((t1_1.a = t2_1.a) AND (t1_1.c = t2_1.c) AND (t1_1.d = t2_1.d))
+               ->  Seq Scan on plt1_adv_m_p1 t1_1
+                     Filter: (a < 10)
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+         ->  Nested Loop Semi Join
+               Join Filter: ((t1_2.a = t2_2.a) AND (t1_2.c = t2_2.c) AND (t1_2.d = t2_2.d))
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+                     Filter: (a < 10)
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+         ->  Nested Loop Semi Join
+               Join Filter: ((t1_3.a = t2_3.a) AND (t1_3.c = t2_3.c) AND (t1_3.d = t2_3.d))
+               ->  Seq Scan on plt1_adv_m_p3 t1_3
+                     Filter: (a < 10)
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+(18 rows)
+
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+ a | b |  c   | d 
+---+---+------+---
+ 3 | 3 | 0003 | 3
+ 4 | 4 | 0004 | 4
+ 6 | 6 | 0006 | 6
+ 9 | 9 | 0009 | 9
+(4 rows)
+
+-- left join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Hash Right Join
+               Hash Cond: ((t2_1.a = t1_1.a) AND (t2_1.c = t1_1.c) AND (t2_1.d = t1_1.d))
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                           Filter: (a < 10)
+         ->  Hash Right Join
+               Hash Cond: ((t2_2.a = t1_2.a) AND (t2_2.c = t1_2.c) AND (t2_2.d = t1_2.d))
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (a < 10)
+         ->  Hash Right Join
+               Hash Cond: ((t2_3.a = t1_3.a) AND (t2_3.c = t1_3.c) AND (t2_3.d = t1_3.d))
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+                           Filter: (a < 10)
+(21 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+ a |  c   | d | a |  c   | d 
+---+------+---+---+------+---
+ 1 | 0001 | 1 |   |      |  
+ 3 | 0003 | 3 | 3 | 0003 | 3
+ 4 | 0004 | 4 | 4 | 0004 | 4
+ 6 | 0006 | 6 | 6 | 0006 | 6
+ 8 | 0008 | 8 |   |      |  
+ 9 | 0009 | 9 | 9 | 0009 | 9
+(6 rows)
+
+-- anti join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+                                         QUERY PLAN                                         
+--------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Nested Loop Anti Join
+               Join Filter: ((t1_1.a = t2_1.a) AND (t1_1.c = t2_1.c) AND (t1_1.d = t2_1.d))
+               ->  Seq Scan on plt1_adv_m_p1 t1_1
+                     Filter: (a < 10)
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+         ->  Nested Loop Anti Join
+               Join Filter: ((t1_2.a = t2_2.a) AND (t1_2.c = t2_2.c) AND (t1_2.d = t2_2.d))
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+                     Filter: (a < 10)
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+         ->  Nested Loop Anti Join
+               Join Filter: ((t1_3.a = t2_3.a) AND (t1_3.c = t2_3.c) AND (t1_3.d = t2_3.d))
+               ->  Seq Scan on plt1_adv_m_p3 t1_3
+                     Filter: (a < 10)
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+(18 rows)
+
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+ a | b |  c   | d 
+---+---+------+---
+ 1 | 1 | 0001 | 1
+ 8 | 8 | 0008 | 8
+(2 rows)
+
+-- full join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.a, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a, t2.a
+   ->  Append
+         ->  Hash Full Join
+               Hash Cond: ((t1_1.a = t2_1.a) AND (t1_1.c = t2_1.c) AND (t1_1.d = t2_1.d))
+               Filter: ((COALESCE(t1_1.a, 0) < 10) AND (COALESCE(t2_1.b, 0) < 10))
+               ->  Seq Scan on plt1_adv_m_p1 t1_1
+               ->  Hash
+                     ->  Seq Scan on plt2_adv_m_p1 t2_1
+         ->  Hash Full Join
+               Hash Cond: ((t1_2.a = t2_2.a) AND (t1_2.c = t2_2.c) AND (t1_2.d = t2_2.d))
+               Filter: ((COALESCE(t1_2.a, 0) < 10) AND (COALESCE(t2_2.b, 0) < 10))
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+               ->  Hash
+                     ->  Seq Scan on plt2_adv_m_p2 t2_2
+         ->  Hash Full Join
+               Hash Cond: ((t1_3.a = t2_3.a) AND (t1_3.c = t2_3.c) AND (t1_3.d = t2_3.d))
+               Filter: ((COALESCE(t1_3.a, 0) < 10) AND (COALESCE(t2_3.b, 0) < 10))
+               ->  Seq Scan on plt1_adv_m_p3 t1_3
+               ->  Hash
+                     ->  Seq Scan on plt2_adv_m_p3 t2_3
+(21 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.a, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+ a |  c   | d | a |  c   | d 
+---+------+---+---+------+---
+ 1 | 0001 | 1 |   |      |  
+ 3 | 0003 | 3 | 3 | 0003 | 3
+ 4 | 0004 | 4 | 4 | 0004 | 4
+ 6 | 0006 | 6 | 6 | 0006 | 6
+ 8 | 0008 | 8 |   |      |  
+ 9 | 0009 | 9 | 9 | 0009 | 9
+   |      |   | 2 | 0002 | 2
+   |      |   | 7 | 0007 | 7
+(8 rows)
+
+-- Test cases where one side has an extra partition
+CREATE TABLE plt2_adv_m_extra PARTITION OF plt2_adv_m FOR VALUES IN (('0000', 0));
+INSERT INTO plt2_adv_m_extra VALUES (0, 0, '0000', 0);
+ANALYZE plt2_adv_m;
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Hash Join
+               Hash Cond: ((t2_1.a = t1_1.a) AND (t2_1.c = t1_1.c) AND (t2_1.d = t1_1.d))
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                           Filter: (b < 10)
+         ->  Hash Join
+               Hash Cond: ((t2_2.a = t1_2.a) AND (t2_2.c = t1_2.c) AND (t2_2.d = t1_2.d))
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (b < 10)
+         ->  Hash Join
+               Hash Cond: ((t2_3.a = t1_3.a) AND (t2_3.c = t1_3.c) AND (t2_3.d = t1_3.d))
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+                           Filter: (b < 10)
+(21 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+ a |  c   | d | a |  c   | d 
+---+------+---+---+------+---
+ 3 | 0003 | 3 | 3 | 0003 | 3
+ 4 | 0004 | 4 | 4 | 0004 | 4
+ 6 | 0006 | 6 | 6 | 0006 | 6
+ 9 | 0009 | 9 | 9 | 0009 | 9
+(4 rows)
+
+-- semi join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+                                         QUERY PLAN                                         
+--------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Nested Loop Semi Join
+               Join Filter: ((t1_1.a = t2_1.a) AND (t1_1.c = t2_1.c) AND (t1_1.d = t2_1.d))
+               ->  Seq Scan on plt1_adv_m_p1 t1_1
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+         ->  Nested Loop Semi Join
+               Join Filter: ((t1_2.a = t2_2.a) AND (t1_2.c = t2_2.c) AND (t1_2.d = t2_2.d))
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+         ->  Nested Loop Semi Join
+               Join Filter: ((t1_3.a = t2_3.a) AND (t1_3.c = t2_3.c) AND (t1_3.d = t2_3.d))
+               ->  Seq Scan on plt1_adv_m_p3 t1_3
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+(18 rows)
+
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+ a | b |  c   | d 
+---+---+------+---
+ 3 | 3 | 0003 | 3
+ 4 | 4 | 0004 | 4
+ 6 | 6 | 0006 | 6
+ 9 | 9 | 0009 | 9
+(4 rows)
+
+-- left join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Hash Right Join
+               Hash Cond: ((t2_1.a = t1_1.a) AND (t2_1.c = t1_1.c) AND (t2_1.d = t1_1.d))
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                           Filter: (b < 10)
+         ->  Hash Right Join
+               Hash Cond: ((t2_2.a = t1_2.a) AND (t2_2.c = t1_2.c) AND (t2_2.d = t1_2.d))
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (b < 10)
+         ->  Hash Right Join
+               Hash Cond: ((t2_3.a = t1_3.a) AND (t2_3.c = t1_3.c) AND (t2_3.d = t1_3.d))
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+                           Filter: (b < 10)
+(21 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+ a |  c   | d | a |  c   | d 
+---+------+---+---+------+---
+ 1 | 0001 | 1 |   |      |  
+ 3 | 0003 | 3 | 3 | 0003 | 3
+ 4 | 0004 | 4 | 4 | 0004 | 4
+ 6 | 0006 | 6 | 6 | 0006 | 6
+ 8 | 0008 | 8 |   |      |  
+ 9 | 0009 | 9 | 9 | 0009 | 9
+(6 rows)
+
+-- left join; currently we can't do partitioned join if there are no matched
+-- partitions on the nullable side
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt2_adv_m t1 LEFT JOIN plt1_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Hash Right Join
+         Hash Cond: ((t2.a = t1.a) AND (t2.c = t1.c) AND (t2.d = t1.d))
+         ->  Append
+               ->  Seq Scan on plt1_adv_m_p1 t2_1
+               ->  Seq Scan on plt1_adv_m_p2 t2_2
+               ->  Seq Scan on plt1_adv_m_p3 t2_3
+         ->  Hash
+               ->  Append
+                     ->  Seq Scan on plt2_adv_m_extra t1_1
+                           Filter: (b < 10)
+                     ->  Seq Scan on plt2_adv_m_p1 t1_2
+                           Filter: (b < 10)
+                     ->  Seq Scan on plt2_adv_m_p2 t1_3
+                           Filter: (b < 10)
+                     ->  Seq Scan on plt2_adv_m_p3 t1_4
+                           Filter: (b < 10)
+(18 rows)
+
+-- anti join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+                                         QUERY PLAN                                         
+--------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Nested Loop Anti Join
+               Join Filter: ((t1_1.a = t2_1.a) AND (t1_1.c = t2_1.c) AND (t1_1.d = t2_1.d))
+               ->  Seq Scan on plt1_adv_m_p1 t1_1
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+         ->  Nested Loop Anti Join
+               Join Filter: ((t1_2.a = t2_2.a) AND (t1_2.c = t2_2.c) AND (t1_2.d = t2_2.d))
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+         ->  Nested Loop Anti Join
+               Join Filter: ((t1_3.a = t2_3.a) AND (t1_3.c = t2_3.c) AND (t1_3.d = t2_3.d))
+               ->  Seq Scan on plt1_adv_m_p3 t1_3
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+(18 rows)
+
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+ a | b |  c   | d 
+---+---+------+---
+ 1 | 1 | 0001 | 1
+ 8 | 8 | 0008 | 8
+(2 rows)
+
+-- anti join; currently we can't do partitioned join if there are no matched
+-- partitions on the nullable side
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt2_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt1_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Hash Anti Join
+         Hash Cond: ((t1.a = t2.a) AND (t1.c = t2.c) AND (t1.d = t2.d))
+         ->  Append
+               ->  Seq Scan on plt2_adv_m_extra t1_1
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_p1 t1_2
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_p2 t1_3
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_p3 t1_4
+                     Filter: (b < 10)
+         ->  Hash
+               ->  Append
+                     ->  Seq Scan on plt1_adv_m_p1 t2_1
+                     ->  Seq Scan on plt1_adv_m_p2 t2_2
+                     ->  Seq Scan on plt1_adv_m_p3 t2_3
+(18 rows)
+
+-- full join; currently we can't do partitioned join if there are no matched
+-- partitions on the nullable side
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a, t2.a
+   ->  Hash Full Join
+         Hash Cond: ((t2.a = t1.a) AND (t2.c = t1.c) AND (t2.d = t1.d))
+         Filter: ((COALESCE(t1.b, 0) < 10) AND (COALESCE(t2.b, 0) < 10))
+         ->  Append
+               ->  Seq Scan on plt2_adv_m_extra t2_1
+               ->  Seq Scan on plt2_adv_m_p1 t2_2
+               ->  Seq Scan on plt2_adv_m_p2 t2_3
+               ->  Seq Scan on plt2_adv_m_p3 t2_4
+         ->  Hash
+               ->  Append
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+(15 rows)
+
+DROP TABLE plt2_adv_m_extra;
+-- Test cases where a partition on one side matches multiple partitions on
+-- the other side; we currently can't do partitioned join in such cases
+ALTER TABLE plt2_adv_m DETACH PARTITION plt2_adv_m_p2;
+-- Split plt2_adv_p2 into two partitions so that plt1_adv_p2 matches both
+CREATE TABLE plt2_adv_m_p2_1 PARTITION OF plt2_adv_m FOR VALUES IN (('0004', 4));
+CREATE TABLE plt2_adv_m_p2_2 PARTITION OF plt2_adv_m FOR VALUES IN (('0006', 6));
+INSERT INTO plt2_adv_m SELECT i, i, to_char(i % 10, 'FM0000'), (i % 10) FROM generate_series(1, 299) i WHERE i % 10 IN (4, 6);
+ANALYZE plt2_adv_m;
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Hash Join
+         Hash Cond: ((t2.a = t1.a) AND (t2.c = t1.c) AND (t2.d = t1.d))
+         ->  Append
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Seq Scan on plt2_adv_m_p2_1 t2_2
+               ->  Seq Scan on plt2_adv_m_p2_2 t2_3
+               ->  Seq Scan on plt2_adv_m_p3 t2_4
+         ->  Hash
+               ->  Append
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                           Filter: (b < 10)
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (b < 10)
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+                           Filter: (b < 10)
+(17 rows)
+
+-- semi join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Hash Semi Join
+         Hash Cond: ((t1.a = t2.a) AND (t1.c = t2.c) AND (t1.d = t2.d))
+         ->  Append
+               ->  Seq Scan on plt1_adv_m_p1 t1_1
+                     Filter: (b < 10)
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+                     Filter: (b < 10)
+               ->  Seq Scan on plt1_adv_m_p3 t1_3
+                     Filter: (b < 10)
+         ->  Hash
+               ->  Append
+                     ->  Seq Scan on plt2_adv_m_p1 t2_1
+                     ->  Seq Scan on plt2_adv_m_p2_1 t2_2
+                     ->  Seq Scan on plt2_adv_m_p2_2 t2_3
+                     ->  Seq Scan on plt2_adv_m_p3 t2_4
+(17 rows)
+
+-- left join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Hash Right Join
+         Hash Cond: ((t2.a = t1.a) AND (t2.c = t1.c) AND (t2.d = t1.d))
+         ->  Append
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Seq Scan on plt2_adv_m_p2_1 t2_2
+               ->  Seq Scan on plt2_adv_m_p2_2 t2_3
+               ->  Seq Scan on plt2_adv_m_p3 t2_4
+         ->  Hash
+               ->  Append
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                           Filter: (b < 10)
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (b < 10)
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+                           Filter: (b < 10)
+(17 rows)
+
+-- anti join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Hash Anti Join
+         Hash Cond: ((t1.a = t2.a) AND (t1.c = t2.c) AND (t1.d = t2.d))
+         ->  Append
+               ->  Seq Scan on plt1_adv_m_p1 t1_1
+                     Filter: (b < 10)
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+                     Filter: (b < 10)
+               ->  Seq Scan on plt1_adv_m_p3 t1_3
+                     Filter: (b < 10)
+         ->  Hash
+               ->  Append
+                     ->  Seq Scan on plt2_adv_m_p1 t2_1
+                     ->  Seq Scan on plt2_adv_m_p2_1 t2_2
+                     ->  Seq Scan on plt2_adv_m_p2_2 t2_3
+                     ->  Seq Scan on plt2_adv_m_p3 t2_4
+(17 rows)
+
+-- full join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a, t2.a
+   ->  Hash Full Join
+         Hash Cond: ((t2.a = t1.a) AND (t2.c = t1.c) AND (t2.d = t1.d))
+         Filter: ((COALESCE(t1.b, 0) < 10) AND (COALESCE(t2.b, 0) < 10))
+         ->  Append
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Seq Scan on plt2_adv_m_p2_1 t2_2
+               ->  Seq Scan on plt2_adv_m_p2_2 t2_3
+               ->  Seq Scan on plt2_adv_m_p3 t2_4
+         ->  Hash
+               ->  Append
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+(15 rows)
+
+DROP TABLE plt2_adv_m_p2_1;
+DROP TABLE plt2_adv_m_p2_2;
+-- Restore plt2_adv_p2
+ALTER TABLE plt2_adv_m ATTACH PARTITION plt2_adv_m_p2 FOR VALUES IN (('0004', 4), ('0006', 6));
+-- Test NULL partitions
+ALTER TABLE plt1_adv_m DETACH PARTITION plt1_adv_m_p1;
+-- Change plt1_adv_p1 to the NULL partition
+CREATE TABLE plt1_adv_m_p1_null PARTITION OF plt1_adv_m FOR VALUES IN ((NULL, NULL), ('0001', 1), ('0003', 3));
+INSERT INTO plt1_adv_m SELECT i, i, to_char(i % 10, 'FM0000'), (i % 10) FROM generate_series(1, 299) i WHERE i % 10 IN (1, 3);
+INSERT INTO plt1_adv_m VALUES (-1, -1, NULL, NULL);
+ANALYZE plt1_adv_m;
+ALTER TABLE plt2_adv_m DETACH PARTITION plt2_adv_m_p3;
+-- Change plt2_adv_p3 to the NULL partition
+CREATE TABLE plt2_adv_m_p3_null PARTITION OF plt2_adv_m FOR VALUES IN ((NULL, NULL), ('0007', 7), ('0009', 9));
+INSERT INTO plt2_adv_m SELECT i, i, to_char(i % 10, 'FM0000'), (i % 10) FROM generate_series(1, 299) i WHERE i % 10 IN (7, 9);
+INSERT INTO plt2_adv_m VALUES (-1, -1, NULL, NULL);
+ANALYZE plt2_adv_m;
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Hash Join
+               Hash Cond: ((t2_1.a = t1_1.a) AND (t2_1.c = t1_1.c) AND (t2_1.d = t1_1.d))
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p1_null t1_1
+                           Filter: (b < 10)
+         ->  Hash Join
+               Hash Cond: ((t2_2.a = t1_2.a) AND (t2_2.c = t1_2.c) AND (t2_2.d = t1_2.d))
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (b < 10)
+         ->  Hash Join
+               Hash Cond: ((t2_3.a = t1_3.a) AND (t2_3.c = t1_3.c) AND (t2_3.d = t1_3.d))
+               ->  Seq Scan on plt2_adv_m_p3_null t2_3
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+                           Filter: (b < 10)
+(21 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+ a |  c   | d | a |  c   | d 
+---+------+---+---+------+---
+ 3 | 0003 | 3 | 3 | 0003 | 3
+ 4 | 0004 | 4 | 4 | 0004 | 4
+ 6 | 0006 | 6 | 6 | 0006 | 6
+ 9 | 0009 | 9 | 9 | 0009 | 9
+(4 rows)
+
+-- semi join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+                                         QUERY PLAN                                         
+--------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Hash Semi Join
+               Hash Cond: ((t1_1.a = t2_1.a) AND (t1_1.c = t2_1.c) AND (t1_1.d = t2_1.d))
+               ->  Seq Scan on plt1_adv_m_p1_null t1_1
+                     Filter: (b < 10)
+               ->  Hash
+                     ->  Seq Scan on plt2_adv_m_p1 t2_1
+         ->  Nested Loop Semi Join
+               Join Filter: ((t1_2.a = t2_2.a) AND (t1_2.c = t2_2.c) AND (t1_2.d = t2_2.d))
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+         ->  Nested Loop Semi Join
+               Join Filter: ((t1_3.a = t2_3.a) AND (t1_3.c = t2_3.c) AND (t1_3.d = t2_3.d))
+               ->  Seq Scan on plt1_adv_m_p3 t1_3
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_p3_null t2_3
+(19 rows)
+
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+ a | b |  c   | d 
+---+---+------+---
+ 3 | 3 | 0003 | 3
+ 4 | 4 | 0004 | 4
+ 6 | 6 | 0006 | 6
+ 9 | 9 | 0009 | 9
+(4 rows)
+
+-- left join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Hash Right Join
+               Hash Cond: ((t2_1.a = t1_1.a) AND (t2_1.c = t1_1.c) AND (t2_1.d = t1_1.d))
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p1_null t1_1
+                           Filter: (b < 10)
+         ->  Hash Right Join
+               Hash Cond: ((t2_2.a = t1_2.a) AND (t2_2.c = t1_2.c) AND (t2_2.d = t1_2.d))
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (b < 10)
+         ->  Hash Right Join
+               Hash Cond: ((t2_3.a = t1_3.a) AND (t2_3.c = t1_3.c) AND (t2_3.d = t1_3.d))
+               ->  Seq Scan on plt2_adv_m_p3_null t2_3
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+                           Filter: (b < 10)
+(21 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+ a  |  c   | d | a |  c   | d 
+----+------+---+---+------+---
+ -1 |      |   |   |      |  
+  1 | 0001 | 1 |   |      |  
+  3 | 0003 | 3 | 3 | 0003 | 3
+  4 | 0004 | 4 | 4 | 0004 | 4
+  6 | 0006 | 6 | 6 | 0006 | 6
+  8 | 0008 | 8 |   |      |  
+  9 | 0009 | 9 | 9 | 0009 | 9
+(7 rows)
+
+-- anti join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+                                         QUERY PLAN                                         
+--------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Hash Anti Join
+               Hash Cond: ((t1_1.a = t2_1.a) AND (t1_1.c = t2_1.c) AND (t1_1.d = t2_1.d))
+               ->  Seq Scan on plt1_adv_m_p1_null t1_1
+                     Filter: (b < 10)
+               ->  Hash
+                     ->  Seq Scan on plt2_adv_m_p1 t2_1
+         ->  Nested Loop Anti Join
+               Join Filter: ((t1_2.a = t2_2.a) AND (t1_2.c = t2_2.c) AND (t1_2.d = t2_2.d))
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+         ->  Nested Loop Anti Join
+               Join Filter: ((t1_3.a = t2_3.a) AND (t1_3.c = t2_3.c) AND (t1_3.d = t2_3.d))
+               ->  Seq Scan on plt1_adv_m_p3 t1_3
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_p3_null t2_3
+(19 rows)
+
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+ a  | b  |  c   | d 
+----+----+------+---
+ -1 | -1 |      |  
+  1 |  1 | 0001 | 1
+  8 |  8 | 0008 | 8
+(3 rows)
+
+-- full join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a, t2.a
+   ->  Append
+         ->  Hash Full Join
+               Hash Cond: ((t1_1.a = t2_1.a) AND (t1_1.c = t2_1.c) AND (t1_1.d = t2_1.d))
+               Filter: ((COALESCE(t1_1.b, 0) < 10) AND (COALESCE(t2_1.b, 0) < 10))
+               ->  Seq Scan on plt1_adv_m_p1_null t1_1
+               ->  Hash
+                     ->  Seq Scan on plt2_adv_m_p1 t2_1
+         ->  Hash Full Join
+               Hash Cond: ((t1_2.a = t2_2.a) AND (t1_2.c = t2_2.c) AND (t1_2.d = t2_2.d))
+               Filter: ((COALESCE(t1_2.b, 0) < 10) AND (COALESCE(t2_2.b, 0) < 10))
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+               ->  Hash
+                     ->  Seq Scan on plt2_adv_m_p2 t2_2
+         ->  Hash Full Join
+               Hash Cond: ((t2_3.a = t1_3.a) AND (t2_3.c = t1_3.c) AND (t2_3.d = t1_3.d))
+               Filter: ((COALESCE(t1_3.b, 0) < 10) AND (COALESCE(t2_3.b, 0) < 10))
+               ->  Seq Scan on plt2_adv_m_p3_null t2_3
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+(21 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+ a  |  c   | d | a  |  c   | d 
+----+------+---+----+------+---
+ -1 |      |   |    |      |  
+  1 | 0001 | 1 |    |      |  
+  3 | 0003 | 3 |  3 | 0003 | 3
+  4 | 0004 | 4 |  4 | 0004 | 4
+  6 | 0006 | 6 |  6 | 0006 | 6
+  8 | 0008 | 8 |    |      |  
+  9 | 0009 | 9 |  9 | 0009 | 9
+    |      |   | -1 |      |  
+    |      |   |  2 | 0002 | 2
+    |      |   |  7 | 0007 | 7
+(10 rows)
+
+DROP TABLE plt1_adv_m_p1_null;
+-- Restore plt1_adv_p1
+ALTER TABLE plt1_adv_m ATTACH PARTITION plt1_adv_m_p1 FOR VALUES IN (('0001', 1), ('0003', 3));
+-- Add to plt1_adv the extra NULL partition containing only NULL values as the
+-- key values
+CREATE TABLE plt1_adv_m_extra PARTITION OF plt1_adv_m FOR VALUES IN ((NULL, NULL));
+INSERT INTO plt1_adv_m VALUES (-1, -1, NULL, NULL);
+ANALYZE plt1_adv_m;
+DROP TABLE plt2_adv_m_p3_null;
+-- Restore plt2_adv_p3
+ALTER TABLE plt2_adv_m ATTACH PARTITION plt2_adv_m_p3 FOR VALUES IN (('0007', 7), ('0009', 9));
+ANALYZE plt2_adv_m;
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Hash Join
+               Hash Cond: ((t2_1.a = t1_1.a) AND (t2_1.c = t1_1.c) AND (t2_1.d = t1_1.d))
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                           Filter: (b < 10)
+         ->  Hash Join
+               Hash Cond: ((t2_2.a = t1_2.a) AND (t2_2.c = t1_2.c) AND (t2_2.d = t1_2.d))
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (b < 10)
+         ->  Hash Join
+               Hash Cond: ((t2_3.a = t1_3.a) AND (t2_3.c = t1_3.c) AND (t2_3.d = t1_3.d))
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+                           Filter: (b < 10)
+(21 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+ a |  c   | d | a |  c   | d 
+---+------+---+---+------+---
+ 3 | 0003 | 3 | 3 | 0003 | 3
+ 4 | 0004 | 4 | 4 | 0004 | 4
+ 6 | 0006 | 6 | 6 | 0006 | 6
+ 9 | 0009 | 9 | 9 | 0009 | 9
+(4 rows)
+
+-- left join; currently we can't do partitioned join if there are no matched
+-- partitions on the nullable side
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Hash Right Join
+         Hash Cond: ((t2.a = t1.a) AND (t2.c = t1.c) AND (t2.d = t1.d))
+         ->  Append
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+         ->  Hash
+               ->  Append
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                           Filter: (b < 10)
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (b < 10)
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+                           Filter: (b < 10)
+                     ->  Seq Scan on plt1_adv_m_extra t1_4
+                           Filter: (b < 10)
+(18 rows)
+
+-- full join; currently we can't do partitioned join if there are no matched
+-- partitions on the nullable side
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a, t2.a
+   ->  Hash Full Join
+         Hash Cond: ((t1.a = t2.a) AND (t1.c = t2.c) AND (t1.d = t2.d))
+         Filter: ((COALESCE(t1.b, 0) < 10) AND (COALESCE(t2.b, 0) < 10))
+         ->  Append
+               ->  Seq Scan on plt1_adv_m_p1 t1_1
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+               ->  Seq Scan on plt1_adv_m_p3 t1_3
+               ->  Seq Scan on plt1_adv_m_extra t1_4
+         ->  Hash
+               ->  Append
+                     ->  Seq Scan on plt2_adv_m_p1 t2_1
+                     ->  Seq Scan on plt2_adv_m_p2 t2_2
+                     ->  Seq Scan on plt2_adv_m_p3 t2_3
+(15 rows)
+
+-- Add to plt2_adv the extra NULL partition containing only NULL values as the
+-- key values
+CREATE TABLE plt2_adv_m_extra PARTITION OF plt2_adv_m FOR VALUES IN ((NULL, NULL));
+INSERT INTO plt2_adv_m VALUES (-1, -1, NULL, NULL);
+ANALYZE plt2_adv_m;
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Hash Join
+               Hash Cond: ((t2_1.a = t1_1.a) AND (t2_1.c = t1_1.c) AND (t2_1.d = t1_1.d))
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                           Filter: (b < 10)
+         ->  Hash Join
+               Hash Cond: ((t2_2.a = t1_2.a) AND (t2_2.c = t1_2.c) AND (t2_2.d = t1_2.d))
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (b < 10)
+         ->  Hash Join
+               Hash Cond: ((t2_3.a = t1_3.a) AND (t2_3.c = t1_3.c) AND (t2_3.d = t1_3.d))
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+                           Filter: (b < 10)
+(21 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+ a |  c   | d | a |  c   | d 
+---+------+---+---+------+---
+ 3 | 0003 | 3 | 3 | 0003 | 3
+ 4 | 0004 | 4 | 4 | 0004 | 4
+ 6 | 0006 | 6 | 6 | 0006 | 6
+ 9 | 0009 | 9 | 9 | 0009 | 9
+(4 rows)
+
+-- left join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+                                         QUERY PLAN                                         
+--------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Hash Right Join
+               Hash Cond: ((t2_1.a = t1_1.a) AND (t2_1.c = t1_1.c) AND (t2_1.d = t1_1.d))
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                           Filter: (b < 10)
+         ->  Hash Right Join
+               Hash Cond: ((t2_2.a = t1_2.a) AND (t2_2.c = t1_2.c) AND (t2_2.d = t1_2.d))
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (b < 10)
+         ->  Hash Right Join
+               Hash Cond: ((t2_3.a = t1_3.a) AND (t2_3.c = t1_3.c) AND (t2_3.d = t1_3.d))
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+                           Filter: (b < 10)
+         ->  Nested Loop Left Join
+               Join Filter: ((t1_4.a = t2_4.a) AND (t1_4.c = t2_4.c) AND (t1_4.d = t2_4.d))
+               ->  Seq Scan on plt1_adv_m_extra t1_4
+                     Filter: (b < 10)
+               ->  Seq Scan on plt2_adv_m_extra t2_4
+(26 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+ a  |  c   | d | a |  c   | d 
+----+------+---+---+------+---
+ -1 |      |   |   |      |  
+  1 | 0001 | 1 |   |      |  
+  3 | 0003 | 3 | 3 | 0003 | 3
+  4 | 0004 | 4 | 4 | 0004 | 4
+  6 | 0006 | 6 | 6 | 0006 | 6
+  8 | 0008 | 8 |   |      |  
+  9 | 0009 | 9 | 9 | 0009 | 9
+(7 rows)
+
+-- full join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a, t2.a
+   ->  Append
+         ->  Hash Full Join
+               Hash Cond: ((t1_1.a = t2_1.a) AND (t1_1.c = t2_1.c) AND (t1_1.d = t2_1.d))
+               Filter: ((COALESCE(t1_1.b, 0) < 10) AND (COALESCE(t2_1.b, 0) < 10))
+               ->  Seq Scan on plt1_adv_m_p1 t1_1
+               ->  Hash
+                     ->  Seq Scan on plt2_adv_m_p1 t2_1
+         ->  Hash Full Join
+               Hash Cond: ((t1_2.a = t2_2.a) AND (t1_2.c = t2_2.c) AND (t1_2.d = t2_2.d))
+               Filter: ((COALESCE(t1_2.b, 0) < 10) AND (COALESCE(t2_2.b, 0) < 10))
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+               ->  Hash
+                     ->  Seq Scan on plt2_adv_m_p2 t2_2
+         ->  Hash Full Join
+               Hash Cond: ((t1_3.a = t2_3.a) AND (t1_3.c = t2_3.c) AND (t1_3.d = t2_3.d))
+               Filter: ((COALESCE(t1_3.b, 0) < 10) AND (COALESCE(t2_3.b, 0) < 10))
+               ->  Seq Scan on plt1_adv_m_p3 t1_3
+               ->  Hash
+                     ->  Seq Scan on plt2_adv_m_p3 t2_3
+         ->  Hash Full Join
+               Hash Cond: ((t1_4.a = t2_4.a) AND (t1_4.c = t2_4.c) AND (t1_4.d = t2_4.d))
+               Filter: ((COALESCE(t1_4.b, 0) < 10) AND (COALESCE(t2_4.b, 0) < 10))
+               ->  Seq Scan on plt1_adv_m_extra t1_4
+               ->  Hash
+                     ->  Seq Scan on plt2_adv_m_extra t2_4
+(27 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+ a  |  c   | d | a  |  c   | d 
+----+------+---+----+------+---
+ -1 |      |   |    |      |  
+  1 | 0001 | 1 |    |      |  
+  3 | 0003 | 3 |  3 | 0003 | 3
+  4 | 0004 | 4 |  4 | 0004 | 4
+  6 | 0006 | 6 |  6 | 0006 | 6
+  8 | 0008 | 8 |    |      |  
+  9 | 0009 | 9 |  9 | 0009 | 9
+    |      |   | -1 |      |  
+    |      |   |  2 | 0002 | 2
+    |      |   |  7 | 0007 | 7
+(10 rows)
+
+-- 3-way join to test the NULL partition of a join relation
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d, t3.a, t3.c, t3.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) LEFT JOIN plt1_adv_m t3 ON (t1.a = t3.a AND t1.c = t3.c AND t1.d = t3.d) WHERE t1.b < 10 ORDER BY t1.a;
+                                              QUERY PLAN                                              
+------------------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Hash Right Join
+               Hash Cond: ((t3_1.a = t1_1.a) AND (t3_1.c = t1_1.c) AND (t3_1.d = t1_1.d))
+               ->  Seq Scan on plt1_adv_m_p1 t3_1
+               ->  Hash
+                     ->  Hash Right Join
+                           Hash Cond: ((t2_1.a = t1_1.a) AND (t2_1.c = t1_1.c) AND (t2_1.d = t1_1.d))
+                           ->  Seq Scan on plt2_adv_m_p1 t2_1
+                           ->  Hash
+                                 ->  Seq Scan on plt1_adv_m_p1 t1_1
+                                       Filter: (b < 10)
+         ->  Hash Right Join
+               Hash Cond: ((t3_2.a = t1_2.a) AND (t3_2.c = t1_2.c) AND (t3_2.d = t1_2.d))
+               ->  Seq Scan on plt1_adv_m_p2 t3_2
+               ->  Hash
+                     ->  Hash Right Join
+                           Hash Cond: ((t2_2.a = t1_2.a) AND (t2_2.c = t1_2.c) AND (t2_2.d = t1_2.d))
+                           ->  Seq Scan on plt2_adv_m_p2 t2_2
+                           ->  Hash
+                                 ->  Seq Scan on plt1_adv_m_p2 t1_2
+                                       Filter: (b < 10)
+         ->  Hash Right Join
+               Hash Cond: ((t3_3.a = t1_3.a) AND (t3_3.c = t1_3.c) AND (t3_3.d = t1_3.d))
+               ->  Seq Scan on plt1_adv_m_p3 t3_3
+               ->  Hash
+                     ->  Hash Right Join
+                           Hash Cond: ((t2_3.a = t1_3.a) AND (t2_3.c = t1_3.c) AND (t2_3.d = t1_3.d))
+                           ->  Seq Scan on plt2_adv_m_p3 t2_3
+                           ->  Hash
+                                 ->  Seq Scan on plt1_adv_m_p3 t1_3
+                                       Filter: (b < 10)
+         ->  Nested Loop Left Join
+               Join Filter: ((t1_4.a = t3_4.a) AND (t1_4.c = t3_4.c) AND (t1_4.d = t3_4.d))
+               ->  Nested Loop Left Join
+                     Join Filter: ((t1_4.a = t2_4.a) AND (t1_4.c = t2_4.c) AND (t1_4.d = t2_4.d))
+                     ->  Seq Scan on plt1_adv_m_extra t1_4
+                           Filter: (b < 10)
+                     ->  Seq Scan on plt2_adv_m_extra t2_4
+               ->  Seq Scan on plt1_adv_m_extra t3_4
+(41 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d, t3.a, t3.c, t3.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) LEFT JOIN plt1_adv_m t3 ON (t1.a = t3.a AND t1.c = t3.c AND t1.d = t3.d) WHERE t1.b < 10 ORDER BY t1.a;
+ a  |  c   | d | a |  c   | d | a |  c   | d 
+----+------+---+---+------+---+---+------+---
+ -1 |      |   |   |      |   |   |      |  
+  1 | 0001 | 1 |   |      |   | 1 | 0001 | 1
+  3 | 0003 | 3 | 3 | 0003 | 3 | 3 | 0003 | 3
+  4 | 0004 | 4 | 4 | 0004 | 4 | 4 | 0004 | 4
+  6 | 0006 | 6 | 6 | 0006 | 6 | 6 | 0006 | 6
+  8 | 0008 | 8 |   |      |   | 8 | 0008 | 8
+  9 | 0009 | 9 | 9 | 0009 | 9 | 9 | 0009 | 9
+(7 rows)
+
+DROP TABLE plt1_adv_m_extra;
+DROP TABLE plt2_adv_m_extra;
+-- Multiple NULL test
+CREATE TABLE plt1_adv_m_p4 PARTITION OF plt1_adv_m FOR VALUES IN (('0005', NULL));
+CREATE TABLE plt1_adv_m_p5 PARTITION OF plt1_adv_m FOR VALUES IN (('0010', NULL), (NULL, 10));
+INSERT INTO plt1_adv_m VALUES (-1, -1, '0005', NULL);
+INSERT INTO plt1_adv_m VALUES (-1, -1, '0010', NULL);
+INSERT INTO plt1_adv_m VALUES (-1, -1, NULL, 10);
+ANALYZE plt1_adv_m;
+CREATE TABLE plt2_adv_m_p4 PARTITION OF plt2_adv_m FOR VALUES IN ((NULL, 5));
+CREATE TABLE plt2_adv_m_p5 PARTITION OF plt2_adv_m FOR VALUES IN (('0010', NULL), (NULL, 10));
+INSERT INTO plt2_adv_m VALUES (-1, -1, '0005', NULL);
+ERROR:  no partition of relation "plt2_adv_m" found for row
+DETAIL:  Partition key of the failing row contains (c, d) = (0005, null).
+INSERT INTO plt2_adv_m VALUES (-1, -1, '0010', NULL);
+INSERT INTO plt2_adv_m VALUES (-1, -1, NULL, 10);
+ANALYZE plt2_adv_m;
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Hash Join
+               Hash Cond: ((t2_1.a = t1_1.a) AND (t2_1.c = t1_1.c) AND (t2_1.d = t1_1.d))
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                           Filter: (a < 10)
+         ->  Hash Join
+               Hash Cond: ((t2_2.a = t1_2.a) AND (t2_2.c = t1_2.c) AND (t2_2.d = t1_2.d))
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (a < 10)
+         ->  Hash Join
+               Hash Cond: ((t2_3.a = t1_3.a) AND (t2_3.c = t1_3.c) AND (t2_3.d = t1_3.d))
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+               ->  Hash
+                     ->  Seq Scan on plt1_adv_m_p3 t1_3
+                           Filter: (a < 10)
+(21 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+ a |  c   | d | a |  c   | d 
+---+------+---+---+------+---
+ 3 | 0003 | 3 | 3 | 0003 | 3
+ 4 | 0004 | 4 | 4 | 0004 | 4
+ 6 | 0006 | 6 | 6 | 0006 | 6
+ 9 | 0009 | 9 | 9 | 0009 | 9
+(4 rows)
+
+-- semi join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+                                         QUERY PLAN                                         
+--------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Append
+         ->  Nested Loop Semi Join
+               Join Filter: ((t1_1.a = t2_1.a) AND (t1_1.c = t2_1.c) AND (t1_1.d = t2_1.d))
+               ->  Seq Scan on plt1_adv_m_p1 t1_1
+                     Filter: (a < 10)
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+         ->  Nested Loop Semi Join
+               Join Filter: ((t1_2.a = t2_2.a) AND (t1_2.c = t2_2.c) AND (t1_2.d = t2_2.d))
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+                     Filter: (a < 10)
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+         ->  Nested Loop Semi Join
+               Join Filter: ((t1_3.a = t2_3.a) AND (t1_3.c = t2_3.c) AND (t1_3.d = t2_3.d))
+               ->  Seq Scan on plt1_adv_m_p3 t1_3
+                     Filter: (a < 10)
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+(18 rows)
+
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+ a | b |  c   | d 
+---+---+------+---
+ 3 | 3 | 0003 | 3
+ 4 | 4 | 0004 | 4
+ 6 | 6 | 0006 | 6
+ 9 | 9 | 0009 | 9
+(4 rows)
+
+-- left join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Hash Right Join
+         Hash Cond: ((t2.a = t1.a) AND (t2.c = t1.c) AND (t2.d = t1.d))
+         ->  Append
+               ->  Seq Scan on plt2_adv_m_p1 t2_1
+               ->  Seq Scan on plt2_adv_m_p2 t2_2
+               ->  Seq Scan on plt2_adv_m_p3 t2_3
+               ->  Seq Scan on plt2_adv_m_p5 t2_4
+               ->  Seq Scan on plt2_adv_m_p4 t2_5
+         ->  Hash
+               ->  Append
+                     ->  Seq Scan on plt1_adv_m_p1 t1_1
+                           Filter: (a < 10)
+                     ->  Seq Scan on plt1_adv_m_p2 t1_2
+                           Filter: (a < 10)
+                     ->  Seq Scan on plt1_adv_m_p4 t1_3
+                           Filter: (a < 10)
+                     ->  Seq Scan on plt1_adv_m_p3 t1_4
+                           Filter: (a < 10)
+                     ->  Seq Scan on plt1_adv_m_p5 t1_5
+                           Filter: (a < 10)
+(22 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+ a  |  c   | d  | a |  c   | d 
+----+------+----+---+------+---
+ -1 | 0010 |    |   |      |  
+ -1 |      | 10 |   |      |  
+ -1 | 0005 |    |   |      |  
+  1 | 0001 |  1 |   |      |  
+  3 | 0003 |  3 | 3 | 0003 | 3
+  4 | 0004 |  4 | 4 | 0004 | 4
+  6 | 0006 |  6 | 6 | 0006 | 6
+  8 | 0008 |  8 |   |      |  
+  9 | 0009 |  9 | 9 | 0009 | 9
+(9 rows)
+
+-- anti join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a
+   ->  Hash Anti Join
+         Hash Cond: ((t1.a = t2.a) AND (t1.c = t2.c) AND (t1.d = t2.d))
+         ->  Append
+               ->  Seq Scan on plt1_adv_m_p1 t1_1
+                     Filter: (a < 10)
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+                     Filter: (a < 10)
+               ->  Seq Scan on plt1_adv_m_p4 t1_3
+                     Filter: (a < 10)
+               ->  Seq Scan on plt1_adv_m_p3 t1_4
+                     Filter: (a < 10)
+               ->  Seq Scan on plt1_adv_m_p5 t1_5
+                     Filter: (a < 10)
+         ->  Hash
+               ->  Append
+                     ->  Seq Scan on plt2_adv_m_p1 t2_1
+                     ->  Seq Scan on plt2_adv_m_p2 t2_2
+                     ->  Seq Scan on plt2_adv_m_p3 t2_3
+                     ->  Seq Scan on plt2_adv_m_p5 t2_4
+                     ->  Seq Scan on plt2_adv_m_p4 t2_5
+(22 rows)
+
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+ a  | b  |  c   | d  
+----+----+------+----
+ -1 | -1 | 0005 |   
+ -1 | -1 | 0010 |   
+ -1 | -1 |      | 10
+  1 |  1 | 0001 |  1
+  8 |  8 | 0008 |  8
+(5 rows)
+
+-- full join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.a, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Sort
+   Sort Key: t1.a, t2.a
+   ->  Hash Full Join
+         Hash Cond: ((t1.a = t2.a) AND (t1.c = t2.c) AND (t1.d = t2.d))
+         Filter: ((COALESCE(t1.a, 0) < 10) AND (COALESCE(t2.b, 0) < 10))
+         ->  Append
+               ->  Seq Scan on plt1_adv_m_p1 t1_1
+               ->  Seq Scan on plt1_adv_m_p2 t1_2
+               ->  Seq Scan on plt1_adv_m_p4 t1_3
+               ->  Seq Scan on plt1_adv_m_p3 t1_4
+               ->  Seq Scan on plt1_adv_m_p5 t1_5
+         ->  Hash
+               ->  Append
+                     ->  Seq Scan on plt2_adv_m_p1 t2_1
+                     ->  Seq Scan on plt2_adv_m_p2 t2_2
+                     ->  Seq Scan on plt2_adv_m_p3 t2_3
+                     ->  Seq Scan on plt2_adv_m_p5 t2_4
+                     ->  Seq Scan on plt2_adv_m_p4 t2_5
+(18 rows)
+
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.a, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+ a  |  c   | d  | a  |  c   | d  
+----+------+----+----+------+----
+ -1 | 0010 |    |    |      |   
+ -1 | 0005 |    |    |      |   
+ -1 |      | 10 |    |      |   
+  1 | 0001 |  1 |    |      |   
+  3 | 0003 |  3 |  3 | 0003 |  3
+  4 | 0004 |  4 |  4 | 0004 |  4
+  6 | 0006 |  6 |  6 | 0006 |  6
+  8 | 0008 |  8 |    |      |   
+  9 | 0009 |  9 |  9 | 0009 |  9
+    |      |    | -1 | 0010 |   
+    |      |    | -1 |      | 10
+    |      |    |  2 | 0002 |  2
+    |      |    |  7 | 0007 |  7
+(13 rows)
+
 -- Tests for multi-level partitioned tables
 CREATE TABLE alpha (a double precision, b int, c text) PARTITION BY RANGE (a);
 CREATE TABLE alpha_neg PARTITION OF alpha FOR VALUES FROM ('-Infinity') TO (0) PARTITION BY RANGE (b);
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7555764..99abf2e 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -168,6 +168,438 @@ explain (costs off) select * from coll_pruning where a collate "POSIX" = 'a' col
          Filter: ((a)::text = 'a'::text COLLATE "POSIX")
 (7 rows)
 
+-- multi-column keys for list partitioning
+create table mc3lp (a int, b text, c int) partition by list (a, b, c);
+create table mc3lp_default partition of mc3lp default;
+create table mc3lp1 partition of mc3lp for values in ((1, 'a', 1), (1, 'b', 1), (5, 'e', 1));
+create table mc3lp2 partition of mc3lp for values in ((4, 'c', 4));
+create table mc3lp3 partition of mc3lp for values in ((5, 'd', 2), (5, 'e', 3), (5, 'f', 4), (8, null, 6));
+create table mc3lp4 partition of mc3lp for values in ((5, 'e', 4), (5, 'e', 5), (5, 'e', 6), (5, 'e', 7));
+create table mc3lp5 partition of mc3lp for values in ((null, 'a', 1), (1, null, 1), (5, 'g', null), (5, 'e', null));
+create table mc3lp6 partition of mc3lp for values in ((null, null, null));
+explain (costs off) select * from mc3lp where a = 4;
+        QUERY PLAN        
+--------------------------
+ Seq Scan on mc3lp2 mc3lp
+   Filter: (a = 4)
+(2 rows)
+
+explain (costs off) select * from mc3lp where a < 4;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (a < 4)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (a < 4)
+   ->  Seq Scan on mc3lp_default mc3lp_3
+         Filter: (a < 4)
+(7 rows)
+
+explain (costs off) select * from mc3lp where a <= 4;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (a <= 4)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (a <= 4)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (a <= 4)
+   ->  Seq Scan on mc3lp_default mc3lp_4
+         Filter: (a <= 4)
+(9 rows)
+
+explain (costs off) select * from mc3lp where a > 4;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (a > 4)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (a > 4)
+   ->  Seq Scan on mc3lp3 mc3lp_3
+         Filter: (a > 4)
+   ->  Seq Scan on mc3lp4 mc3lp_4
+         Filter: (a > 4)
+   ->  Seq Scan on mc3lp_default mc3lp_5
+         Filter: (a > 4)
+(11 rows)
+
+explain (costs off) select * from mc3lp where a >= 4;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (a >= 4)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (a >= 4)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (a >= 4)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (a >= 4)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (a >= 4)
+   ->  Seq Scan on mc3lp_default mc3lp_6
+         Filter: (a >= 4)
+(13 rows)
+
+explain (costs off) select * from mc3lp where a is null;
+            QUERY PLAN            
+----------------------------------
+ Append
+   ->  Seq Scan on mc3lp5 mc3lp_1
+         Filter: (a IS NULL)
+   ->  Seq Scan on mc3lp6 mc3lp_2
+         Filter: (a IS NULL)
+(5 rows)
+
+explain (costs off) select * from mc3lp where a is not null;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (a IS NOT NULL)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (a IS NOT NULL)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (a IS NOT NULL)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (a IS NOT NULL)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (a IS NOT NULL)
+   ->  Seq Scan on mc3lp_default mc3lp_6
+         Filter: (a IS NOT NULL)
+(13 rows)
+
+explain (costs off) select * from mc3lp where b = 'c';
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b = 'c'::text)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b = 'c'::text)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b = 'c'::text)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b = 'c'::text)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b = 'c'::text)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b = 'c'::text)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b = 'c'::text)
+(15 rows)
+
+explain (costs off) select * from mc3lp where b < 'c';
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b < 'c'::text)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b < 'c'::text)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b < 'c'::text)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b < 'c'::text)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b < 'c'::text)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b < 'c'::text)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b < 'c'::text)
+(15 rows)
+
+explain (costs off) select * from mc3lp where b <= 'c';
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b <= 'c'::text)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b <= 'c'::text)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b <= 'c'::text)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b <= 'c'::text)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b <= 'c'::text)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b <= 'c'::text)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b <= 'c'::text)
+(15 rows)
+
+explain (costs off) select * from mc3lp where b > 'c';
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b > 'c'::text)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b > 'c'::text)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b > 'c'::text)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b > 'c'::text)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b > 'c'::text)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b > 'c'::text)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b > 'c'::text)
+(15 rows)
+
+explain (costs off) select * from mc3lp where b >= 'c';
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b >= 'c'::text)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b >= 'c'::text)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b >= 'c'::text)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b >= 'c'::text)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b >= 'c'::text)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b >= 'c'::text)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b >= 'c'::text)
+(15 rows)
+
+explain (costs off) select * from mc3lp where b is null;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b IS NULL)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b IS NULL)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b IS NULL)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b IS NULL)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b IS NULL)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b IS NULL)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b IS NULL)
+(15 rows)
+
+explain (costs off) select * from mc3lp where b is not null;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b IS NOT NULL)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b IS NOT NULL)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b IS NOT NULL)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b IS NOT NULL)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b IS NOT NULL)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b IS NOT NULL)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b IS NOT NULL)
+(15 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e';
+                  QUERY PLAN                   
+-----------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: ((a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp3 mc3lp_3
+         Filter: ((a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp4 mc3lp_4
+         Filter: ((a = 5) AND (b = 'e'::text))
+(9 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b < 'e';
+               QUERY PLAN                
+-----------------------------------------
+ Seq Scan on mc3lp3 mc3lp
+   Filter: ((b < 'e'::text) AND (a = 5))
+(2 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b > 'e';
+                  QUERY PLAN                   
+-----------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp5 mc3lp_1
+         Filter: ((b > 'e'::text) AND (a = 5))
+   ->  Seq Scan on mc3lp3 mc3lp_2
+         Filter: ((b > 'e'::text) AND (a = 5))
+(5 rows)
+
+explain (costs off) select * from mc3lp where a is null and b is null;
+               QUERY PLAN                
+-----------------------------------------
+ Seq Scan on mc3lp6 mc3lp
+   Filter: ((a IS NULL) AND (b IS NULL))
+(2 rows)
+
+explain (costs off) select * from mc3lp where a is not null and b is not null;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((a IS NOT NULL) AND (b IS NOT NULL))
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: ((a IS NOT NULL) AND (b IS NOT NULL))
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: ((a IS NOT NULL) AND (b IS NOT NULL))
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: ((a IS NOT NULL) AND (b IS NOT NULL))
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: ((a IS NOT NULL) AND (b IS NOT NULL))
+   ->  Seq Scan on mc3lp_default mc3lp_6
+         Filter: ((a IS NOT NULL) AND (b IS NOT NULL))
+(13 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and c = 2;
+              QUERY PLAN               
+---------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((a = 5) AND (c = 2))
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: ((a = 5) AND (c = 2))
+   ->  Seq Scan on mc3lp3 mc3lp_3
+         Filter: ((a = 5) AND (c = 2))
+   ->  Seq Scan on mc3lp4 mc3lp_4
+         Filter: ((a = 5) AND (c = 2))
+(9 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and c < 2;
+              QUERY PLAN               
+---------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((c < 2) AND (a = 5))
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: ((c < 2) AND (a = 5))
+   ->  Seq Scan on mc3lp3 mc3lp_3
+         Filter: ((c < 2) AND (a = 5))
+   ->  Seq Scan on mc3lp4 mc3lp_4
+         Filter: ((c < 2) AND (a = 5))
+(9 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and c > 2;
+              QUERY PLAN               
+---------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((c > 2) AND (a = 5))
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: ((c > 2) AND (a = 5))
+   ->  Seq Scan on mc3lp3 mc3lp_3
+         Filter: ((c > 2) AND (a = 5))
+   ->  Seq Scan on mc3lp4 mc3lp_4
+         Filter: ((c > 2) AND (a = 5))
+(9 rows)
+
+explain (costs off) select * from mc3lp where a is null and c is null;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp5 mc3lp_1
+         Filter: ((a IS NULL) AND (c IS NULL))
+   ->  Seq Scan on mc3lp6 mc3lp_2
+         Filter: ((a IS NULL) AND (c IS NULL))
+(5 rows)
+
+explain (costs off) select * from mc3lp where a is not null and c is not null;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((a IS NOT NULL) AND (c IS NOT NULL))
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: ((a IS NOT NULL) AND (c IS NOT NULL))
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: ((a IS NOT NULL) AND (c IS NOT NULL))
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: ((a IS NOT NULL) AND (c IS NOT NULL))
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: ((a IS NOT NULL) AND (c IS NOT NULL))
+   ->  Seq Scan on mc3lp_default mc3lp_6
+         Filter: ((a IS NOT NULL) AND (c IS NOT NULL))
+(13 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c = 4;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Seq Scan on mc3lp4 mc3lp
+   Filter: ((a = 5) AND (b = 'e'::text) AND (c = 4))
+(2 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c < 4;
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((c < 4) AND (a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp3 mc3lp_2
+         Filter: ((c < 4) AND (a = 5) AND (b = 'e'::text))
+(5 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c <= 4;
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((c <= 4) AND (a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp3 mc3lp_2
+         Filter: ((c <= 4) AND (a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp4 mc3lp_3
+         Filter: ((c <= 4) AND (a = 5) AND (b = 'e'::text))
+(7 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c > 4;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Seq Scan on mc3lp4 mc3lp
+   Filter: ((c > 4) AND (a = 5) AND (b = 'e'::text))
+(2 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c >= 4;
+                      QUERY PLAN                      
+------------------------------------------------------
+ Seq Scan on mc3lp4 mc3lp
+   Filter: ((c >= 4) AND (a = 5) AND (b = 'e'::text))
+(2 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c is null;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Seq Scan on mc3lp5 mc3lp
+   Filter: ((c IS NULL) AND (a = 5) AND (b = 'e'::text))
+(2 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c is not null;
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((c IS NOT NULL) AND (a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp3 mc3lp_2
+         Filter: ((c IS NOT NULL) AND (a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp4 mc3lp_3
+         Filter: ((c IS NOT NULL) AND (a = 5) AND (b = 'e'::text))
+(7 rows)
+
 create table rlp (a int, b varchar) partition by range (a);
 create table rlp_default partition of rlp default partition by list (a);
 create table rlp_default_default partition of rlp_default default;
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index cc41f58..34e7e34 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -342,12 +342,6 @@ CREATE TABLE partitioned (
 	a int
 ) INHERITS (some_table) PARTITION BY LIST (a);
 
--- cannot use more than 1 column as partition key for list partitioned table
-CREATE TABLE partitioned (
-	a1 int,
-	a2 int
-) PARTITION BY LIST (a1, a2);	-- fail
-
 -- unsupported constraint type for partitioned tables
 CREATE TABLE partitioned (
 	a int,
@@ -562,6 +556,9 @@ CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES WITH (MODULUS 10, REM
 CREATE TABLE part_default PARTITION OF list_parted DEFAULT;
 CREATE TABLE fail_default_part PARTITION OF list_parted DEFAULT;
 
+-- trying to specify more number of values than the number of partition keys
+CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN ((1, 2));
+
 -- specified literal can't be cast to the partition column data type
 CREATE TABLE bools (
 	a bool
@@ -728,6 +725,32 @@ CREATE TABLE range3_default PARTITION OF range_parted3 DEFAULT;
 -- more specific ranges
 CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, minvalue) TO (1, maxvalue);
 
+-- now check for multi-column list partition key
+CREATE TABLE list_parted3 (
+	a int,
+	b varchar
+) PARTITION BY LIST (a, b);
+
+CREATE TABLE list_parted3_p1 PARTITION OF list_parted3 FOR VALUES IN ((1, 'A'));
+CREATE TABLE list_parted3_p2 PARTITION OF list_parted3 FOR VALUES IN ((1, 'B'),(1, 'E'), (1, 'E'), (2, 'C'),(2, 'D'));
+CREATE TABLE list_parted3_p3 PARTITION OF list_parted3 FOR VALUES IN ((1, NULL),(NULL, 'F'));
+CREATE TABLE list_parted3_p4 PARTITION OF list_parted3 FOR VALUES IN ((NULL, NULL));
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((1, 'E'));
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((1, NULL));
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((NULL, 'F'));
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((NULL, NULL));
+CREATE TABLE list_parted3_default PARTITION OF list_parted3 DEFAULT;
+
+-- trying to specify less number of values than the number of partition keys
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN (10, 'N');
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((10), ('N'));
+
+-- trying to specify more number of values than the number of partition keys
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((10, 'N', 10));
+
+-- cleanup
+DROP TABLE list_parted3;
+
 -- check for partition bound overlap and other invalid specifications for the hash partition
 CREATE TABLE hash_parted2 (
 	a varchar
diff --git a/src/test/regress/sql/insert.sql b/src/test/regress/sql/insert.sql
index bfaa8a3..2bfc55c 100644
--- a/src/test/regress/sql/insert.sql
+++ b/src/test/regress/sql/insert.sql
@@ -536,6 +536,48 @@ select tableoid::regclass::text, * from mcrparted order by 1;
 -- cleanup
 drop table mcrparted;
 
+-- Test multi-column list partitioning with 3 partition keys
+create table mclparted (a int, b text, c int) partition by list (a, b, c);
+create table mclparted_p1 partition of mclparted for values in ((1, 'a', 1));
+create table mclparted_p2 partition of mclparted for values in ((1, 'a', 2), (1, 'b', 1), (2, 'a', 1));
+create table mclparted_p3 partition of mclparted for values in ((3, 'c', 3), (4, 'd', 4), (5, 'e', 5), (6, null, 6));
+create table mclparted_p4 partition of mclparted for values in ((null, 'a', 1), (1, null, 1), (1, 'a', null));
+create table mclparted_p5 partition of mclparted for values in ((null, null, null));
+
+-- routed to mclparted_p1
+insert into mclparted values (1, 'a', 1);
+
+-- routed to mclparted_p2
+insert into mclparted values (1, 'a', 2);
+insert into mclparted values (1, 'b', 1);
+insert into mclparted values (2, 'a', 1);
+
+-- routed to mclparted_p3
+insert into mclparted values (3, 'c', 3);
+insert into mclparted values (4, 'd', 4);
+insert into mclparted values (5, 'e', 5);
+insert into mclparted values (6, null, 6);
+
+-- routed to mclparted_p4
+insert into mclparted values (null, 'a', 1);
+insert into mclparted values (1, null, 1);
+insert into mclparted values (1, 'a', null);
+
+-- routed to mclparted_p5
+insert into mclparted values (null, null, null);
+
+-- error cases
+insert into mclparted values (10, 'a', 1);
+insert into mclparted values (1, 'z', 1);
+insert into mclparted values (1, 'a', 10);
+insert into mclparted values (1, null, null);
+
+-- check rows
+select tableoid::regclass::text, * from mclparted order by 1, 2, 3, 4;
+
+-- cleanup
+drop table mclparted;
+
 -- check that a BR constraint can't make partition contain violating rows
 create table brtrigpartcon (a int, b text) partition by list (a);
 create table brtrigpartcon1 partition of brtrigpartcon for values in (1);
@@ -612,6 +654,28 @@ insert into mcrparted values ('aaa', 0), ('b', 0), ('bz', 10), ('c', -10),
 select tableoid::regclass, * from mcrparted order by a, b;
 drop table mcrparted;
 
+-- check multi-column list partitioning with partition key constraint
+create table mclparted (a text, b int) partition by list(a, b);
+create table mclparted_p1 partition of mclparted for values in (('a', 1));
+create table mclparted_p2 partition of mclparted for values in (('a', 2), ('b', 1), ('c', 3), ('d', 3), ('e', 3));
+create table mclparted_p3 partition of mclparted for values in (('a', 3), ('a', 4), ('a', null), (null, 1));
+create table mclparted_p4 partition of mclparted for values in (('b', null), (null, 2));
+create table mclparted_p5 partition of mclparted for values in ((null, null));
+create table mclparted_p6 partition of mclparted DEFAULT;
+
+\d+ mclparted
+\d+ mclparted_p1
+\d+ mclparted_p2
+\d+ mclparted_p3
+\d+ mclparted_p4
+\d+ mclparted_p5
+
+insert into mclparted values ('a', 1), ('a', 2), ('b', 1), ('c', 3), ('d', 3),
+	('e', 3), ('a', 3), ('a', 4), ('a', null), (null, 1), ('b', null),
+	(null, 2), (null, null), ('z', 10);
+select tableoid::regclass, * from mclparted order by a, b;
+drop table mclparted;
+
 -- check that wholerow vars in the RETURNING list work with partitioned tables
 create table returningwrtest (a int) partition by list (a);
 create table returningwrtest1 partition of returningwrtest for values in (1);
diff --git a/src/test/regress/sql/partition_join.sql b/src/test/regress/sql/partition_join.sql
index d97b5b6..ca0ec38 100644
--- a/src/test/regress/sql/partition_join.sql
+++ b/src/test/regress/sql/partition_join.sql
@@ -1100,6 +1100,263 @@ DROP TABLE plt2_adv;
 DROP TABLE plt3_adv;
 
 
+-- Tests for multi-column list-partitioned tables
+CREATE TABLE plt1_adv_m (a int, b int, c text, d int) PARTITION BY LIST (c, d);
+CREATE TABLE plt1_adv_m_p1 PARTITION OF plt1_adv_m FOR VALUES IN (('0001', 1), ('0003', 3));
+CREATE TABLE plt1_adv_m_p2 PARTITION OF plt1_adv_m FOR VALUES IN (('0004', 4), ('0006', 6));
+CREATE TABLE plt1_adv_m_p3 PARTITION OF plt1_adv_m FOR VALUES IN (('0008', 8), ('0009', 9));
+INSERT INTO plt1_adv_m SELECT i, i, to_char(i % 10, 'FM0000'), (i % 10) FROM generate_series(1, 299) i WHERE i % 10 IN (1, 3, 4, 6, 8, 9);
+ANALYZE plt1_adv_m;
+
+CREATE TABLE plt2_adv_m (a int, b int, c text, d int) PARTITION BY LIST (c, d);
+CREATE TABLE plt2_adv_m_p1 PARTITION OF plt2_adv_m FOR VALUES IN (('0002', 2), ('0003', 3));
+CREATE TABLE plt2_adv_m_p2 PARTITION OF plt2_adv_m FOR VALUES IN (('0004', 4), ('0006', 6));
+CREATE TABLE plt2_adv_m_p3 PARTITION OF plt2_adv_m FOR VALUES IN (('0007', 7), ('0009', 9));
+INSERT INTO plt2_adv_m SELECT i, i, to_char(i % 10, 'FM0000'), (i % 10) FROM generate_series(1, 299) i WHERE i % 10 IN (2, 3, 4, 6, 7, 9);
+ANALYZE plt2_adv_m;
+
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+
+-- semi join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+
+-- left join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+
+-- anti join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+
+-- full join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.a, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.a, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+
+-- Test cases where one side has an extra partition
+CREATE TABLE plt2_adv_m_extra PARTITION OF plt2_adv_m FOR VALUES IN (('0000', 0));
+INSERT INTO plt2_adv_m_extra VALUES (0, 0, '0000', 0);
+ANALYZE plt2_adv_m;
+
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+
+-- semi join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+
+-- left join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+
+-- left join; currently we can't do partitioned join if there are no matched
+-- partitions on the nullable side
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt2_adv_m t1 LEFT JOIN plt1_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+
+-- anti join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+
+-- anti join; currently we can't do partitioned join if there are no matched
+-- partitions on the nullable side
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt2_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt1_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+
+-- full join; currently we can't do partitioned join if there are no matched
+-- partitions on the nullable side
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+
+DROP TABLE plt2_adv_m_extra;
+
+-- Test cases where a partition on one side matches multiple partitions on
+-- the other side; we currently can't do partitioned join in such cases
+ALTER TABLE plt2_adv_m DETACH PARTITION plt2_adv_m_p2;
+-- Split plt2_adv_p2 into two partitions so that plt1_adv_p2 matches both
+CREATE TABLE plt2_adv_m_p2_1 PARTITION OF plt2_adv_m FOR VALUES IN (('0004', 4));
+CREATE TABLE plt2_adv_m_p2_2 PARTITION OF plt2_adv_m FOR VALUES IN (('0006', 6));
+INSERT INTO plt2_adv_m SELECT i, i, to_char(i % 10, 'FM0000'), (i % 10) FROM generate_series(1, 299) i WHERE i % 10 IN (4, 6);
+ANALYZE plt2_adv_m;
+
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+
+-- semi join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+
+-- left join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+
+-- anti join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+
+-- full join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+
+DROP TABLE plt2_adv_m_p2_1;
+DROP TABLE plt2_adv_m_p2_2;
+-- Restore plt2_adv_p2
+ALTER TABLE plt2_adv_m ATTACH PARTITION plt2_adv_m_p2 FOR VALUES IN (('0004', 4), ('0006', 6));
+
+
+-- Test NULL partitions
+ALTER TABLE plt1_adv_m DETACH PARTITION plt1_adv_m_p1;
+-- Change plt1_adv_p1 to the NULL partition
+CREATE TABLE plt1_adv_m_p1_null PARTITION OF plt1_adv_m FOR VALUES IN ((NULL, NULL), ('0001', 1), ('0003', 3));
+INSERT INTO plt1_adv_m SELECT i, i, to_char(i % 10, 'FM0000'), (i % 10) FROM generate_series(1, 299) i WHERE i % 10 IN (1, 3);
+INSERT INTO plt1_adv_m VALUES (-1, -1, NULL, NULL);
+ANALYZE plt1_adv_m;
+
+ALTER TABLE plt2_adv_m DETACH PARTITION plt2_adv_m_p3;
+-- Change plt2_adv_p3 to the NULL partition
+CREATE TABLE plt2_adv_m_p3_null PARTITION OF plt2_adv_m FOR VALUES IN ((NULL, NULL), ('0007', 7), ('0009', 9));
+INSERT INTO plt2_adv_m SELECT i, i, to_char(i % 10, 'FM0000'), (i % 10) FROM generate_series(1, 299) i WHERE i % 10 IN (7, 9);
+INSERT INTO plt2_adv_m VALUES (-1, -1, NULL, NULL);
+ANALYZE plt2_adv_m;
+
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+
+-- semi join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+
+-- left join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+
+-- anti join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.b < 10 ORDER BY t1.a;
+
+-- full join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+
+DROP TABLE plt1_adv_m_p1_null;
+-- Restore plt1_adv_p1
+ALTER TABLE plt1_adv_m ATTACH PARTITION plt1_adv_m_p1 FOR VALUES IN (('0001', 1), ('0003', 3));
+
+-- Add to plt1_adv the extra NULL partition containing only NULL values as the
+-- key values
+CREATE TABLE plt1_adv_m_extra PARTITION OF plt1_adv_m FOR VALUES IN ((NULL, NULL));
+INSERT INTO plt1_adv_m VALUES (-1, -1, NULL, NULL);
+ANALYZE plt1_adv_m;
+
+DROP TABLE plt2_adv_m_p3_null;
+-- Restore plt2_adv_p3
+ALTER TABLE plt2_adv_m ATTACH PARTITION plt2_adv_m_p3 FOR VALUES IN (('0007', 7), ('0009', 9));
+ANALYZE plt2_adv_m;
+
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+
+-- left join; currently we can't do partitioned join if there are no matched
+-- partitions on the nullable side
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+
+-- full join; currently we can't do partitioned join if there are no matched
+-- partitions on the nullable side
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+
+
+-- Add to plt2_adv the extra NULL partition containing only NULL values as the
+-- key values
+CREATE TABLE plt2_adv_m_extra PARTITION OF plt2_adv_m FOR VALUES IN ((NULL, NULL));
+INSERT INTO plt2_adv_m VALUES (-1, -1, NULL, NULL);
+ANALYZE plt2_adv_m;
+
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+
+-- left join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.b < 10 ORDER BY t1.a;
+
+-- full join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.b, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+
+-- 3-way join to test the NULL partition of a join relation
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d, t3.a, t3.c, t3.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) LEFT JOIN plt1_adv_m t3 ON (t1.a = t3.a AND t1.c = t3.c AND t1.d = t3.d) WHERE t1.b < 10 ORDER BY t1.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d, t3.a, t3.c, t3.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) LEFT JOIN plt1_adv_m t3 ON (t1.a = t3.a AND t1.c = t3.c AND t1.d = t3.d) WHERE t1.b < 10 ORDER BY t1.a;
+
+DROP TABLE plt1_adv_m_extra;
+DROP TABLE plt2_adv_m_extra;
+
+-- Multiple NULL test
+CREATE TABLE plt1_adv_m_p4 PARTITION OF plt1_adv_m FOR VALUES IN (('0005', NULL));
+CREATE TABLE plt1_adv_m_p5 PARTITION OF plt1_adv_m FOR VALUES IN (('0010', NULL), (NULL, 10));
+INSERT INTO plt1_adv_m VALUES (-1, -1, '0005', NULL);
+INSERT INTO plt1_adv_m VALUES (-1, -1, '0010', NULL);
+INSERT INTO plt1_adv_m VALUES (-1, -1, NULL, 10);
+ANALYZE plt1_adv_m;
+
+CREATE TABLE plt2_adv_m_p4 PARTITION OF plt2_adv_m FOR VALUES IN ((NULL, 5));
+CREATE TABLE plt2_adv_m_p5 PARTITION OF plt2_adv_m FOR VALUES IN (('0010', NULL), (NULL, 10));
+INSERT INTO plt2_adv_m VALUES (-1, -1, '0005', NULL);
+INSERT INTO plt2_adv_m VALUES (-1, -1, '0010', NULL);
+INSERT INTO plt2_adv_m VALUES (-1, -1, NULL, 10);
+ANALYZE plt2_adv_m;
+
+-- inner join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 INNER JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+
+-- semi join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+SELECT t1.* FROM plt1_adv_m t1 WHERE EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+
+-- left join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 LEFT JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE t1.a < 10 ORDER BY t1.a;
+
+-- anti join
+EXPLAIN (COSTS OFF)
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+SELECT t1.* FROM plt1_adv_m t1 WHERE NOT EXISTS (SELECT 1 FROM plt2_adv_m t2 WHERE t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) AND t1.a < 10 ORDER BY t1.a;
+
+-- full join
+EXPLAIN (COSTS OFF)
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.a, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+SELECT t1.a, t1.c, t1.d, t2.a, t2.c, t2.d FROM plt1_adv_m t1 FULL JOIN plt2_adv_m t2 ON (t1.a = t2.a AND t1.c = t2.c AND t1.d = t2.d) WHERE coalesce(t1.a, 0) < 10 AND coalesce(t2.b, 0) < 10 ORDER BY t1.a, t2.a;
+
 -- Tests for multi-level partitioned tables
 CREATE TABLE alpha (a double precision, b int, c text) PARTITION BY RANGE (a);
 CREATE TABLE alpha_neg PARTITION OF alpha FOR VALUES FROM ('-Infinity') TO (0) PARTITION BY RANGE (b);
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index d70bd86..da2762e 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -34,6 +34,48 @@ explain (costs off) select * from coll_pruning where a collate "C" = 'a' collate
 -- collation doesn't match the partitioning collation, no pruning occurs
 explain (costs off) select * from coll_pruning where a collate "POSIX" = 'a' collate "POSIX";
 
+-- multi-column keys for list partitioning
+create table mc3lp (a int, b text, c int) partition by list (a, b, c);
+create table mc3lp_default partition of mc3lp default;
+create table mc3lp1 partition of mc3lp for values in ((1, 'a', 1), (1, 'b', 1), (5, 'e', 1));
+create table mc3lp2 partition of mc3lp for values in ((4, 'c', 4));
+create table mc3lp3 partition of mc3lp for values in ((5, 'd', 2), (5, 'e', 3), (5, 'f', 4), (8, null, 6));
+create table mc3lp4 partition of mc3lp for values in ((5, 'e', 4), (5, 'e', 5), (5, 'e', 6), (5, 'e', 7));
+create table mc3lp5 partition of mc3lp for values in ((null, 'a', 1), (1, null, 1), (5, 'g', null), (5, 'e', null));
+create table mc3lp6 partition of mc3lp for values in ((null, null, null));
+
+explain (costs off) select * from mc3lp where a = 4;
+explain (costs off) select * from mc3lp where a < 4;
+explain (costs off) select * from mc3lp where a <= 4;
+explain (costs off) select * from mc3lp where a > 4;
+explain (costs off) select * from mc3lp where a >= 4;
+explain (costs off) select * from mc3lp where a is null;
+explain (costs off) select * from mc3lp where a is not null;
+explain (costs off) select * from mc3lp where b = 'c';
+explain (costs off) select * from mc3lp where b < 'c';
+explain (costs off) select * from mc3lp where b <= 'c';
+explain (costs off) select * from mc3lp where b > 'c';
+explain (costs off) select * from mc3lp where b >= 'c';
+explain (costs off) select * from mc3lp where b is null;
+explain (costs off) select * from mc3lp where b is not null;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e';
+explain (costs off) select * from mc3lp where a = 5 and b < 'e';
+explain (costs off) select * from mc3lp where a = 5 and b > 'e';
+explain (costs off) select * from mc3lp where a is null and b is null;
+explain (costs off) select * from mc3lp where a is not null and b is not null;
+explain (costs off) select * from mc3lp where a = 5 and c = 2;
+explain (costs off) select * from mc3lp where a = 5 and c < 2;
+explain (costs off) select * from mc3lp where a = 5 and c > 2;
+explain (costs off) select * from mc3lp where a is null and c is null;
+explain (costs off) select * from mc3lp where a is not null and c is not null;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c = 4;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c < 4;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c <= 4;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c > 4;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c >= 4;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c is null;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c is not null;
+
 create table rlp (a int, b varchar) partition by range (a);
 create table rlp_default partition of rlp default partition by list (a);
 create table rlp_default_default partition of rlp_default default;
-- 
1.8.3.1

