Multi-Column List Partitioning

Started by Nitin Jadhavover 4 years ago52 messages
#1Nitin Jadhav
nitinjadhavpostgres@gmail.com
1 attachment(s)

Hi,

While reviewing one of the 'Table partitioning' related patches, I found
that Postgres does not support multiple column based LIST partitioning.
Based on this understanding, I have started working on this feature. I also
feel that 'Multi-Column List Partitioning' can be benefited to the Postgres
users in future.

I am attaching the WIP patch for this feature here. It supports
'Multi-Column List Partitioning', however some tasks are still pending. I
would like to know your thoughts about this, So that I can continue the
work with improvising the current patch.

Following things are handled in the patch.
1. Syntax

CREATE TABLE table_name (attrs) PARTITION BY LIST(list_of_columns);

Earlier there was no provision to mention multiple columns as part of the
'list_of_columns' clause. Now we can mention the list of columns separated
by comma.

CREATE TABLE table_name_p1 PARTITION OF table_name FOR VALUES IN
list_of_values.

Whereas list_of_columns can be
a. (value [,...])
b. (value [,...]) [,...]

I would like to list a few examples here for better understanding.
Ex-1:
CREATE TABLE t1(a int) PARTITION BY LIST(a);
CREATE TABLE t1_1 PARTITION OF t1 FOR VALUES IN (1, 2, 10, 5, 7);

Ex-2:
CREATE TABLE t2(a int, b int) PARTITION BY LIST(a,b);
CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES IN (1, 2), (1, 5), (2, 2),(2,
10);

Please share if any changes are required in the above syntax.

2. Modified transformation logic to support above syntax.

3. Modified the data structures to store the information caused by above
syntax. Also modified the searching logic to route the tuple to the
appropriate partition.

4. Done a few basic testing and verified CREATE TABLE, INSERT INTO and
SELECT are working fine.

Following items are pending and I am working on it.

1. Handling of 'NULL' values.

2. Support multi column case in partition pruning.

3. Add test cases to the regression test suite.

Please share your thoughts.

Thanks & Regards,
Nitin Jadhav

Attachments:

v0_multi_column_list_partitioning.patchapplication/octet-stream; name=v0_multi_column_list_partitioning.patchDownload
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d9ba87a..e85adc5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16422,13 +16422,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 8afddca..bbe97b1 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -1272,7 +1272,8 @@ get_partition_for_tuple(PartitionDispatch pd, Datum *values, bool *isnull)
 				bound_offset = partition_list_bsearch(key->partsupfunc,
 													  key->partcollation,
 													  boundinfo,
-													  values[0], &equal);
+													  key->partnatts,
+													  values, &equal);
 				if (bound_offset >= 0 && equal)
 					part_index = boundinfo->indexes[bound_offset];
 			}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index b4ab401..466801a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -607,6 +607,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <list>		part_params
 %type <partboundspec> PartitionBoundSpec
 %type <list>		hash_partbound
+%type <list>		list_values
 %type <defelt>		hash_partbound_elem
 
 %type <str>	optColumnCompression
@@ -2880,13 +2881,13 @@ PartitionBoundSpec:
 				}
 
 			/* a LIST partition */
-			| FOR VALUES IN_P '(' expr_list ')'
+			| FOR VALUES IN_P list_values
 				{
 					PartitionBoundSpec *n = makeNode(PartitionBoundSpec);
 
 					n->strategy = PARTITION_STRATEGY_LIST;
 					n->is_default = false;
-					n->listdatums = $5;
+					n->listdatums = $4;
 					n->location = @3;
 
 					$$ = n;
@@ -2936,6 +2937,17 @@ hash_partbound:
 			}
 		;
 
+list_values:
+		'(' expr_list ')'
+			{
+				$$ = list_make1($2);
+			}
+		| list_values ',' '(' expr_list ')'
+			{
+				$$ = lappend($1, $4);
+			}
+		;
+
 /*****************************************************************************
  *
  *	ALTER TYPE
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 9dd3037..14956bc 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -146,6 +146,11 @@ 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,
+										  char *colname, Oid coltype,
+										  int32 coltypmod, Oid partcollation,
+										  int partnatts);
 
 
 /*
@@ -4015,6 +4020,41 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 }
 
 /*
+ * checkForDuplicates
+ *
+ * Return TRUE if the element is already present in the list.
+ * FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)
+{
+	ListCell   *cell;
+
+	foreach(cell, source)
+	{
+		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(searchElem, i));
+
+			if (!equal(value1, value2)){
+				isDuplicate = false;
+				break;
+			}
+		}
+
+		if (isDuplicate)
+			return true;
+	}
+
+	return false;
+}
+
+/*
  * transformPartitionBound
  *
  * Transform a partition bound specification
@@ -4077,7 +4117,6 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 	}
 	else if (strategy == PARTITION_STRATEGY_LIST)
 	{
-		ListCell   *cell;
 		char	   *colname;
 		Oid			coltype;
 		int32		coltypmod;
@@ -4103,36 +4142,10 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 		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 = castNode(Const, lfirst(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, colname, coltype,
+										 coltypmod, partcollation,
+										 key->partnatts);
 	}
 	else if (strategy == PARTITION_STRATEGY_RANGE)
 	{
@@ -4168,6 +4181,72 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 	return result_spec;
 }
 
+static List *
+transformPartitionListBounds(ParseState *pstate, PartitionBoundSpec *spec,
+							 char *colname, Oid coltype, int32 coltypmod,
+							 Oid partcollation, int partnatts)
+{
+	ListCell   *cell;
+	List	   *result = NIL;
+
+	foreach(cell, spec->listdatums)
+	{
+		List	   *elem = lfirst(cell);
+		List	   *value = NIL;
+		ListCell   *cell2;
+		bool		isDuplicate;
+
+		if (partnatts == 1 && list_length(spec->listdatums) == 1)
+		{
+			foreach(cell2, elem)
+			{
+				List	   *value = NIL;
+				Node	   *expr = lfirst(cell2);
+				Const	   *val;
+
+				val = transformPartitionBoundValue(pstate, expr, colname,
+												   coltype, coltypmod,
+												   partcollation);
+
+				value = lappend(value, val);
+
+				isDuplicate = checkForDuplicates(result, value);
+				if (isDuplicate)
+					continue;
+
+				result = lappend(result, value);
+			}
+		}
+		else
+		{
+			if (partnatts !=  list_length(elem))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("Must specify exactly one value per partitioning column"),
+						 parser_errposition(pstate, exprLocation((Node *) spec))));
+
+			foreach(cell2, elem)
+			{
+				Node       *expr = lfirst(cell2);
+				Const      *val;
+
+				val = transformPartitionBoundValue(pstate, expr, colname,
+												   coltype, coltypmod,
+												   partcollation);
+				value = lappend(value, val);
+			}
+
+			isDuplicate = checkForDuplicates(result, value);
+			if (isDuplicate)
+				continue;
+
+			result = lappend(result, value);
+		}
+	}
+
+	return result;
+}
+
 /*
  * transformPartitionRangeBounds
  *		This converts the expressions for range partition bounds from the raw
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index c9c7892..2056cc3 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -57,7 +57,7 @@ typedef struct PartitionHashBound
 typedef struct PartitionListValue
 {
 	int			index;
-	Datum		value;
+	Datum	   *values;
 } PartitionListValue;
 
 /* One bound of a range partition */
@@ -239,6 +239,12 @@ static void get_range_key_properties(PartitionKey key, int keynum,
 									 Expr **keyCol,
 									 Const **lower_val, Const **upper_val);
 static List *get_range_nulltest(PartitionKey key);
+static int partition_list_bserach_complete(FmgrInfo *partsupfunc,
+										   Oid *partcollation,
+										   PartitionBoundInfo boundinfo,
+										   int nvalues,
+										   Datum *values, bool *is_equal,
+										   int mid);
 
 /*
  * get_qual_from_partbound
@@ -444,6 +450,7 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	PartitionListValue **all_values = NULL;
 	ListCell   *cell;
 	int			i = 0;
+	int			j = 0;
 	int			ndatums = 0;
 	int			next_index = 0;
 	int			default_index = -1;
@@ -479,25 +486,22 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 
 		foreach(c, spec->listdatums)
 		{
-			Const	   *val = castNode(Const, lfirst(c));
+			List	   *elem = lfirst(c);
+			ListCell   *cell;
 			PartitionListValue *list_value = NULL;
 
-			if (!val->constisnull)
-			{
-				list_value = (PartitionListValue *)
-					palloc0(sizeof(PartitionListValue));
-				list_value->index = i;
-				list_value->value = val->constvalue;
-			}
-			else
+			list_value = (PartitionListValue *)
+				palloc0(sizeof(PartitionListValue));
+			list_value->index = i;
+			list_value->values = (Datum *) palloc0(key->partnatts * sizeof(Datum));
+
+			j = 0;
+			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 = castNode(Const, lfirst(cell));
+
+				list_value->values[j] = val->constvalue;
+				j++;
 			}
 
 			if (list_value)
@@ -520,7 +524,7 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 
 		all_values[i] = (PartitionListValue *)
 			palloc(sizeof(PartitionListValue));
-		all_values[i]->value = src->value;
+		all_values[i]->values = src->values;
 		all_values[i]->index = src->index;
 		i++;
 	}
@@ -542,11 +546,12 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	for (i = 0; i < ndatums; i++)
 	{
 		int			orig_index = all_values[i]->index;
+		boundinfo->datums[i] = (Datum *) palloc(key->partnatts * sizeof(Datum));
 
-		boundinfo->datums[i] = (Datum *) palloc(sizeof(Datum));
-		boundinfo->datums[i][0] = datumCopy(all_values[i]->value,
-											key->parttypbyval[0],
-											key->parttyplen[0]);
+		for (j = 0; j < key->partnatts; j++)
+			boundinfo->datums[i][j] = datumCopy(all_values[i]->values[j],
+												key->parttypbyval[0],
+												key->parttyplen[0]);
 
 		/* If the old index has no mapping, assign one */
 		if ((*mapping)[orig_index] == -1)
@@ -922,9 +927,6 @@ 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->kind != NULL)
@@ -2950,32 +2952,31 @@ check_new_partition_bound(char *relname, Relation parent,
 
 					foreach(cell, spec->listdatums)
 					{
-						Const	   *val = castNode(Const, lfirst(cell));
+						int			i;
+						int         offset;
+						bool        equal;
+						List	   *elem = lfirst(cell);
+						Datum	   *values = (Datum *) palloc0(key->partnatts * sizeof(Datum));;
 
-						overlap_location = val->location;
-						if (!val->constisnull)
+						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;
 						}
-						else if (partition_bound_accepts_nulls(boundinfo))
+
+						offset = partition_list_bsearch(&key->partsupfunc[0],
+														key->partcollation,
+														boundinfo,
+														key->partnatts,
+														values,
+														&equal);
+						if (offset >= 0 && equal)
 						{
 							overlap = true;
-							with = boundinfo->null_index;
+							with = boundinfo->indexes[offset];
 							break;
 						}
+						pfree(values);
 					}
 				}
 
@@ -3488,6 +3489,109 @@ partition_hbound_cmp(int modulus1, int remainder1, int modulus2, int remainder2)
 }
 
 /*
+ * partition_lbound_datum_cmp
+ *
+ * This function compares the list bound values of all the partition key
+ * columns. Returns TRUE if all the values are matching. Returns FALSE if any
+ * of the value is not matching.
+ *
+ */
+static bool
+partition_lbound_datum_cmp(FmgrInfo *partsupfunc, Oid *partcollation,
+						   Datum *lb_datums, int nvalues, Datum *values)
+{
+	int i = 0;
+	int32 cmpval = 0;
+
+	for (i = 1; i < nvalues; i++)
+	{
+		cmpval = DatumGetInt32(FunctionCall2Coll(&partsupfunc[0],
+												 partcollation[0],
+												 lb_datums[i],
+												 values[i]));
+		if (cmpval != 0)
+			return false;
+	}
+
+	return true;
+}
+
+/*
+ * partition_list_bserach_complete
+ *
+ * This function is called once it finds the first match for first value of the
+ * list. Then this function will continue the serach and return the index of
+ * matching element if found.
+ *
+ */
+static int
+partition_list_bserach_complete(FmgrInfo *partsupfunc, Oid *partcollation,
+								PartitionBoundInfo boundinfo, int nvalues,
+								Datum *values, bool *is_equal, int mid)
+{
+
+	int32 res = partition_lbound_datum_cmp(partsupfunc, partcollation,
+										   boundinfo->datums[mid],
+										   nvalues, values);
+	if (res == true)
+	{
+		*is_equal = true;
+		return mid;
+	}
+	else
+	{
+		int i;
+		int idx = mid;
+
+		for (i = mid-1; i >= 0; i--)
+		{
+			idx--;
+			res = DatumGetInt32(FunctionCall2Coll(&partsupfunc[0],
+												  partcollation[0],
+												  boundinfo->datums[i][0],
+												  values[0]));
+			if (res != 0)
+				break;
+
+			res = partition_lbound_datum_cmp(partsupfunc, partcollation,
+											 boundinfo->datums[i],
+											 nvalues, values);
+			if (res == true)
+			{
+				*is_equal = true;
+				return idx;
+			}
+		}
+
+		idx = mid;
+		for (i = mid+1; i < boundinfo->ndatums; i++)
+		{
+			idx++;
+			res = DatumGetInt32(FunctionCall2Coll(&partsupfunc[0],
+												  partcollation[0],
+												  boundinfo->datums[i][0],
+												  values[0]));
+
+			if (res != 0)
+				break;
+
+			res = partition_lbound_datum_cmp(partsupfunc, partcollation,
+											 boundinfo->datums[i],
+											 nvalues, values);
+
+			if (res == true)
+			{
+				*is_equal = true;
+				return idx;
+			}
+		}
+
+		*is_equal = false;
+		return -1;
+	}
+}
+
+/*
  * 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
@@ -3497,8 +3601,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, int nvalues,
+					   Datum *values, bool *is_equal)
 {
 	int			lo,
 				hi,
@@ -3514,7 +3618,16 @@ partition_list_bsearch(FmgrInfo *partsupfunc, Oid *partcollation,
 		cmpval = DatumGetInt32(FunctionCall2Coll(&partsupfunc[0],
 												 partcollation[0],
 												 boundinfo->datums[mid][0],
-												 value));
+												 values[0]));
+
+		if (cmpval == 0)
+		{
+			return partition_list_bserach_complete(partsupfunc, partcollation,
+												   boundinfo, nvalues, values,
+												   is_equal, mid);
+		}
+
+
 		if (cmpval <= 0)
 		{
 			lo = mid;
@@ -3684,13 +3797,14 @@ 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	   *val1 = (*(PartitionListValue *const *) a)->values;
+	Datum	   *val2 = (*(PartitionListValue *const *) b)->values;
+
 	PartitionKey key = (PartitionKey) arg;
 
 	return DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[0],
 										   key->partcollation[0],
-										   val1, val2));
+										   val1[0], val2[0]));
 }
 
 /*
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index c793742..fbc9e29 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -2728,7 +2728,7 @@ get_matching_list_bounds(PartitionPruneContext *context,
 											  boundinfo->ndatums - 1);
 
 		off = partition_list_bsearch(partsupfunc, partcollation, boundinfo,
-									 value, &is_equal);
+									 nvalues, value, &is_equal);
 		if (off >= 0 && is_equal)
 		{
 
@@ -2760,8 +2760,8 @@ get_matching_list_bounds(PartitionPruneContext *context,
 		case BTEqualStrategyNumber:
 			off = partition_list_bsearch(partsupfunc,
 										 partcollation,
-										 boundinfo, value,
-										 &is_equal);
+										 boundinfo, nvalues,
+										 value, &is_equal);
 			if (off >= 0 && is_equal)
 			{
 				Assert(boundinfo->indexes[off] >= 0);
@@ -2777,8 +2777,8 @@ get_matching_list_bounds(PartitionPruneContext *context,
 		case BTGreaterStrategyNumber:
 			off = partition_list_bsearch(partsupfunc,
 										 partcollation,
-										 boundinfo, value,
-										 &is_equal);
+										 boundinfo, nvalues,
+										 value, &is_equal);
 			if (off >= 0)
 			{
 				/* We don't want the matched datum to be in the result. */
@@ -2812,8 +2812,8 @@ get_matching_list_bounds(PartitionPruneContext *context,
 		case BTLessStrategyNumber:
 			off = partition_list_bsearch(partsupfunc,
 										 partcollation,
-										 boundinfo, value,
-										 &is_equal);
+										 boundinfo, nvalues,
+										 value, &is_equal);
 			if (off >= 0 && is_equal && !inclusive)
 				off--;
 
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 0a4fa93..b22d39d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9394,18 +9394,27 @@ get_rule_expr(Node *node, deparse_context *context,
 					case PARTITION_STRATEGY_LIST:
 						Assert(spec->listdatums != NIL);
 
-						appendStringInfoString(buf, "FOR VALUES IN (");
+						appendStringInfoString(buf, "FOR VALUES IN ");
 						sep = "";
 						foreach(cell, spec->listdatums)
 						{
-							Const	   *val = castNode(Const, lfirst(cell));
+							ListCell   *cell2;
+							List	   *elem = lfirst(cell);
 
 							appendStringInfoString(buf, sep);
-							get_const_expr(val, context, -1);
+							appendStringInfoChar(buf, '(');
+							sep = "";
+							foreach(cell2, elem)
+							{
+								Const	   *val = castNode(Const, lfirst(cell2));
+
+								appendStringInfoString(buf, sep);
+								get_const_expr(val, context, -1);
+								sep = ", ";
+							}
+							appendStringInfoChar(buf, ')');
 							sep = ", ";
 						}
-
-						appendStringInfoChar(buf, ')');
 						break;
 
 					case PARTITION_STRATEGY_RANGE:
diff --git a/src/include/partitioning/partbounds.h b/src/include/partitioning/partbounds.h
index ebf3ff1..7bf20ff 100644
--- a/src/include/partitioning/partbounds.h
+++ b/src/include/partitioning/partbounds.h
@@ -117,7 +117,7 @@ extern int32 partition_rbound_datum_cmp(FmgrInfo *partsupfunc,
 extern int	partition_list_bsearch(FmgrInfo *partsupfunc,
 								   Oid *partcollation,
 								   PartitionBoundInfo boundinfo,
-								   Datum value, bool *is_equal);
+								   int nvalues, Datum *value, bool *is_equal);
 extern int	partition_range_datum_bsearch(FmgrInfo *partsupfunc,
 										  Oid *partcollation,
 										  PartitionBoundInfo boundinfo,
#2Jeevan Ladhe
jeevan.ladhe@enterprisedb.com
In reply to: Nitin Jadhav (#1)
Re: Multi-Column List Partitioning

While reviewing one of the 'Table partitioning' related patches,
I found that Postgres does not support multiple column based LIST
partitioning. Based on this understanding, I have started working on
this feature. I also feel that 'Multi-Column List Partitioning' can
be benefited to the Postgres users in future.

+1 for the feature. I also think this can help users deal with some
useful cases.

CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES IN (1, 2), (1, 5), (2,
2),(2, 10);

IMHO, listing every single tuple like this might be a bit cumbersome for
the user. What about something like this:

...FOR VALUES IN (1, 2, 3, 4), (11, 22, 33, 44), where the first set
is the list for values of column A and second list is for column B. We
can treat these lists as A X B possible values or simply (a1, b1), (a2,
b2) internally. However I see other proprietary databases already have
syntax something similar that you are proposing here. So, I leave it
open for the thoughts from experts. Also, though what I propose might be
easy from a user perspective, but might not be that easy for
implementation, given that for a larger number of columns in partition list
e.g. A X B X C X D lists become unmanageable.

I did not review the patch in detail, but a quick look at it leaves me
with following comments:

1.

+ * list. Then this function will continue the serach and return the

index of
Typo:
s/serach/search

2.
A compiler warning:
partprune.c: In function ‘get_matching_list_bounds’:
partprune.c:2731:20: error: passing argument 5 of ‘partition_list_bsearch’
makes pointer from integer without a cast [-Werror=int-conversion]
2731 | nvalues, value, &is_equal);
| ^~~~~
| |
| Datum {aka long unsigned int}
In file included from partprune.c:53:
../../../src/include/partitioning/partbounds.h:120:32: note: expected
‘Datum *’ {aka ‘long unsigned int *’} but argument is of type ‘Datum’ {aka
‘long unsigned int’}
120 | int nvalues, Datum *value, bool *is_equal);
| ~~~~~~~^~~~~

3.
And, a server crash with following case:
postgres=# CREATE TABLE t1 (a int) PARTITION BY LIST (a);
CREATE TABLE
postgres=# CREATE TABLE t1p1 PARTITION OF t1 FOR VALUES IN (1, 2, 3);
CREATE TABLE
postgres=# \d+ t1p1
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
The connection to the server was lost. Attempting reset: Failed.
!?>

Stacktrace:
(gdb) bt
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1 0x00007f5d273c5859 in __GI_abort () at abort.c:79
#2 0x000055779d2eb69d in ExceptionalCondition
(conditionName=0x55779d4978d8 "ptr == NULL || nodeTag(ptr) == type",
errorType=0x55779d4978c3 "FailedAssertion",
fileName=0x55779d4978a0 "../../../src/include/nodes/nodes.h",
lineNumber=603) at assert.c:69
#3 0x000055779d03a684 in castNodeImpl (type=T_Const, ptr=0x55779e457b18)
at ../../../src/include/nodes/nodes.h:603
#4 0x000055779d04368a in get_qual_for_list (parent=0x7f5d1df829b8,
spec=0x55779e457950) at partbounds.c:4155
#5 0x000055779d03ac60 in get_qual_from_partbound (rel=0x7f5d1df82570,
parent=0x7f5d1df829b8, spec=0x55779e457950) at partbounds.c:272
#6 0x000055779d2cf630 in generate_partition_qual (rel=0x7f5d1df82570) at
partcache.c:379
#7 0x000055779d2cf468 in get_partition_qual_relid (relid=32771) at
partcache.c:308
#8 0x000055779d2592bf in pg_get_partition_constraintdef
(fcinfo=0x55779e44ee50) at ruleutils.c:2019
#9 0x000055779cec7221 in ExecInterpExpr (state=0x55779e44dfb0,
econtext=0x55779e407fe8, isnull=0x7ffddf9b109f) at execExprInterp.c:744
#10 0x000055779cec954f in ExecInterpExprStillValid (state=0x55779e44dfb0,
econtext=0x55779e407fe8, isNull=0x7ffddf9b109f) at execExprInterp.c:1819
#11 0x000055779cf1d58a in ExecEvalExprSwitchContext (state=0x55779e44dfb0,
econtext=0x55779e407fe8, isNull=0x7ffddf9b109f)
at ../../../src/include/executor/executor.h:338
#12 0x000055779cf1d602 in ExecProject (projInfo=0x55779e44dfa8) at
../../../src/include/executor/executor.h:372
#13 0x000055779cf1db2f in ExecNestLoop (pstate=0x55779e407ed0) at
nodeNestloop.c:241
#14 0x000055779cedf136 in ExecProcNodeFirst (node=0x55779e407ed0) at
execProcnode.c:462
#15 0x000055779ced3053 in ExecProcNode (node=0x55779e407ed0) at
../../../src/include/executor/executor.h:257
#16 0x000055779ced5a87 in ExecutePlan (estate=0x55779e407c80,
planstate=0x55779e407ed0, use_parallel_mode=false, operation=CMD_SELECT,
sendTuples=true, numberTuples=0,
direction=ForwardScanDirection, dest=0x55779e425a88, execute_once=true)
at execMain.c:1551
#17 0x000055779ced372d in standard_ExecutorRun (queryDesc=0x55779e453520,
direction=ForwardScanDirection, count=0, execute_once=true) at
execMain.c:361
#18 0x000055779ced353c in ExecutorRun (queryDesc=0x55779e453520,
direction=ForwardScanDirection, count=0, execute_once=true) at
execMain.c:305
#19 0x000055779d13d287 in PortalRunSelect (portal=0x55779e398800,
forward=true, count=0, dest=0x55779e425a88) at pquery.c:912
#20 0x000055779d13cec0 in PortalRun (portal=0x55779e398800,
count=9223372036854775807, isTopLevel=true, run_once=true,
dest=0x55779e425a88, altdest=0x55779e425a88,
qc=0x7ffddf9b14f0) at pquery.c:756
#21 0x000055779d1361ce in exec_simple_query (
query_string=0x55779e3367a0 "SELECT inhparent::pg_catalog.regclass,\n
pg_catalog.pg_get_expr(c.relpartbound, c.oid),\n inhdetachpending,\n
pg_catalog.pg_get_partition_constraintdef(c.oid)\nFROM pg_catalog.pg_class
c JOIN pg_catalo"...) at postgres.c:1214
#22 0x000055779d13ad8b in PostgresMain (argc=1, argv=0x7ffddf9b1710,
dbname=0x55779e3626f8 "postgres", username=0x55779e3626d8 "hadoop") at
postgres.c:4476
#23 0x000055779d0674d3 in BackendRun (port=0x55779e358380) at
postmaster.c:4488
#24 0x000055779d066d8c in BackendStartup (port=0x55779e358380) at
postmaster.c:4210
#25 0x000055779d062f9b in ServerLoop () at postmaster.c:1742
#26 0x000055779d062734 in PostmasterMain (argc=3, argv=0x55779e3308b0) at
postmaster.c:1414
#27 0x000055779cf5805f in main (argc=3, argv=0x55779e3308b0) at main.c:209

Regards,
Jeevan Ladhe

On Thu, May 6, 2021 at 7:33 PM Nitin Jadhav <nitinjadhavpostgres@gmail.com>
wrote:

Show quoted text

Hi,

While reviewing one of the 'Table partitioning' related patches, I found
that Postgres does not support multiple column based LIST partitioning.
Based on this understanding, I have started working on this feature. I also
feel that 'Multi-Column List Partitioning' can be benefited to the Postgres
users in future.

I am attaching the WIP patch for this feature here. It supports
'Multi-Column List Partitioning', however some tasks are still pending. I
would like to know your thoughts about this, So that I can continue the
work with improvising the current patch.

Following things are handled in the patch.
1. Syntax

CREATE TABLE table_name (attrs) PARTITION BY LIST(list_of_columns);

Earlier there was no provision to mention multiple columns as part of the
'list_of_columns' clause. Now we can mention the list of columns separated
by comma.

CREATE TABLE table_name_p1 PARTITION OF table_name FOR VALUES IN
list_of_values.

Whereas list_of_columns can be
a. (value [,...])
b. (value [,...]) [,...]

I would like to list a few examples here for better understanding.
Ex-1:
CREATE TABLE t1(a int) PARTITION BY LIST(a);
CREATE TABLE t1_1 PARTITION OF t1 FOR VALUES IN (1, 2, 10, 5, 7);

Ex-2:
CREATE TABLE t2(a int, b int) PARTITION BY LIST(a,b);
CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES IN (1, 2), (1, 5), (2, 2),(2,
10);

Please share if any changes are required in the above syntax.

2. Modified transformation logic to support above syntax.

3. Modified the data structures to store the information caused by above
syntax. Also modified the searching logic to route the tuple to the
appropriate partition.

4. Done a few basic testing and verified CREATE TABLE, INSERT INTO and
SELECT are working fine.

Following items are pending and I am working on it.

1. Handling of 'NULL' values.

2. Support multi column case in partition pruning.

3. Add test cases to the regression test suite.

Please share your thoughts.

Thanks & Regards,
Nitin Jadhav

#3Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Jeevan Ladhe (#2)
Re: Multi-Column List Partitioning

Thanks Jeevan for looking into this thread.

I did not review the patch in detail, but a quick look at it leaves me
with following comments:

I will incorporate these changes.

...FOR VALUES IN (1, 2, 3, 4), (11, 22, 33, 44), where the first set
is the list for values of column A and second list is for column B. We
can treat these lists as A X B possible values or simply (a1, b1), (a2,
b2) internally. However I see other proprietary databases already have
syntax something similar that you are proposing here. So, I leave it
open for the thoughts from experts. Also, though what I propose might be
easy from a user perspective, but might not be that easy for
implementation, given that for a larger number of columns in partition

list

e.g. A X B X C X D lists become unmanageable.

I feel this is also not easy from a user's perspective. For example
for a partition
with 2 partition keys (a,b) for values like (1,1), (1,2), (1,3),
(1,4),(1,5). This
would be converted to (1,1,1,1,1), (1,2,3,4,5). It is difficult to match
the values
of column 'a' to 'b'. Anyways let's wait for the other's opinion about this.

Thanks & Regards,
Nitin Jadhav

On Fri, May 7, 2021 at 7:36 PM Jeevan Ladhe <jeevan.ladhe@enterprisedb.com>
wrote:

Show quoted text

While reviewing one of the 'Table partitioning' related patches,
I found that Postgres does not support multiple column based LIST
partitioning. Based on this understanding, I have started working on
this feature. I also feel that 'Multi-Column List Partitioning' can
be benefited to the Postgres users in future.

+1 for the feature. I also think this can help users deal with some
useful cases.

CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES IN (1, 2), (1, 5), (2,
2),(2, 10);

IMHO, listing every single tuple like this might be a bit cumbersome for
the user. What about something like this:

...FOR VALUES IN (1, 2, 3, 4), (11, 22, 33, 44), where the first set
is the list for values of column A and second list is for column B. We
can treat these lists as A X B possible values or simply (a1, b1), (a2,
b2) internally. However I see other proprietary databases already have
syntax something similar that you are proposing here. So, I leave it
open for the thoughts from experts. Also, though what I propose might be
easy from a user perspective, but might not be that easy for
implementation, given that for a larger number of columns in partition list
e.g. A X B X C X D lists become unmanageable.

I did not review the patch in detail, but a quick look at it leaves me
with following comments:

1.

+ * list. Then this function will continue the serach and return the

index of
Typo:
s/serach/search

2.
A compiler warning:
partprune.c: In function ‘get_matching_list_bounds’:
partprune.c:2731:20: error: passing argument 5 of ‘partition_list_bsearch’
makes pointer from integer without a cast [-Werror=int-conversion]
2731 | nvalues, value, &is_equal);
| ^~~~~
| |
| Datum {aka long unsigned int}
In file included from partprune.c:53:
../../../src/include/partitioning/partbounds.h:120:32: note: expected
‘Datum *’ {aka ‘long unsigned int *’} but argument is of type ‘Datum’ {aka
‘long unsigned int’}
120 | int nvalues, Datum *value, bool *is_equal);
| ~~~~~~~^~~~~

3.
And, a server crash with following case:
postgres=# CREATE TABLE t1 (a int) PARTITION BY LIST (a);
CREATE TABLE
postgres=# CREATE TABLE t1p1 PARTITION OF t1 FOR VALUES IN (1, 2, 3);
CREATE TABLE
postgres=# \d+ t1p1
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
The connection to the server was lost. Attempting reset: Failed.
!?>

Stacktrace:
(gdb) bt
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1 0x00007f5d273c5859 in __GI_abort () at abort.c:79
#2 0x000055779d2eb69d in ExceptionalCondition
(conditionName=0x55779d4978d8 "ptr == NULL || nodeTag(ptr) == type",
errorType=0x55779d4978c3 "FailedAssertion",
fileName=0x55779d4978a0 "../../../src/include/nodes/nodes.h",
lineNumber=603) at assert.c:69
#3 0x000055779d03a684 in castNodeImpl (type=T_Const, ptr=0x55779e457b18)
at ../../../src/include/nodes/nodes.h:603
#4 0x000055779d04368a in get_qual_for_list (parent=0x7f5d1df829b8,
spec=0x55779e457950) at partbounds.c:4155
#5 0x000055779d03ac60 in get_qual_from_partbound (rel=0x7f5d1df82570,
parent=0x7f5d1df829b8, spec=0x55779e457950) at partbounds.c:272
#6 0x000055779d2cf630 in generate_partition_qual (rel=0x7f5d1df82570) at
partcache.c:379
#7 0x000055779d2cf468 in get_partition_qual_relid (relid=32771) at
partcache.c:308
#8 0x000055779d2592bf in pg_get_partition_constraintdef
(fcinfo=0x55779e44ee50) at ruleutils.c:2019
#9 0x000055779cec7221 in ExecInterpExpr (state=0x55779e44dfb0,
econtext=0x55779e407fe8, isnull=0x7ffddf9b109f) at execExprInterp.c:744
#10 0x000055779cec954f in ExecInterpExprStillValid (state=0x55779e44dfb0,
econtext=0x55779e407fe8, isNull=0x7ffddf9b109f) at execExprInterp.c:1819
#11 0x000055779cf1d58a in ExecEvalExprSwitchContext (state=0x55779e44dfb0,
econtext=0x55779e407fe8, isNull=0x7ffddf9b109f)
at ../../../src/include/executor/executor.h:338
#12 0x000055779cf1d602 in ExecProject (projInfo=0x55779e44dfa8) at
../../../src/include/executor/executor.h:372
#13 0x000055779cf1db2f in ExecNestLoop (pstate=0x55779e407ed0) at
nodeNestloop.c:241
#14 0x000055779cedf136 in ExecProcNodeFirst (node=0x55779e407ed0) at
execProcnode.c:462
#15 0x000055779ced3053 in ExecProcNode (node=0x55779e407ed0) at
../../../src/include/executor/executor.h:257
#16 0x000055779ced5a87 in ExecutePlan (estate=0x55779e407c80,
planstate=0x55779e407ed0, use_parallel_mode=false, operation=CMD_SELECT,
sendTuples=true, numberTuples=0,
direction=ForwardScanDirection, dest=0x55779e425a88,
execute_once=true) at execMain.c:1551
#17 0x000055779ced372d in standard_ExecutorRun (queryDesc=0x55779e453520,
direction=ForwardScanDirection, count=0, execute_once=true) at
execMain.c:361
#18 0x000055779ced353c in ExecutorRun (queryDesc=0x55779e453520,
direction=ForwardScanDirection, count=0, execute_once=true) at
execMain.c:305
#19 0x000055779d13d287 in PortalRunSelect (portal=0x55779e398800,
forward=true, count=0, dest=0x55779e425a88) at pquery.c:912
#20 0x000055779d13cec0 in PortalRun (portal=0x55779e398800,
count=9223372036854775807, isTopLevel=true, run_once=true,
dest=0x55779e425a88, altdest=0x55779e425a88,
qc=0x7ffddf9b14f0) at pquery.c:756
#21 0x000055779d1361ce in exec_simple_query (
query_string=0x55779e3367a0 "SELECT inhparent::pg_catalog.regclass,\n
pg_catalog.pg_get_expr(c.relpartbound, c.oid),\n inhdetachpending,\n
pg_catalog.pg_get_partition_constraintdef(c.oid)\nFROM pg_catalog.pg_class
c JOIN pg_catalo"...) at postgres.c:1214
#22 0x000055779d13ad8b in PostgresMain (argc=1, argv=0x7ffddf9b1710,
dbname=0x55779e3626f8 "postgres", username=0x55779e3626d8 "hadoop") at
postgres.c:4476
#23 0x000055779d0674d3 in BackendRun (port=0x55779e358380) at
postmaster.c:4488
#24 0x000055779d066d8c in BackendStartup (port=0x55779e358380) at
postmaster.c:4210
#25 0x000055779d062f9b in ServerLoop () at postmaster.c:1742
#26 0x000055779d062734 in PostmasterMain (argc=3, argv=0x55779e3308b0) at
postmaster.c:1414
#27 0x000055779cf5805f in main (argc=3, argv=0x55779e3308b0) at main.c:209

Regards,
Jeevan Ladhe

On Thu, May 6, 2021 at 7:33 PM Nitin Jadhav <nitinjadhavpostgres@gmail.com>
wrote:

Hi,

While reviewing one of the 'Table partitioning' related patches, I found
that Postgres does not support multiple column based LIST partitioning.
Based on this understanding, I have started working on this feature. I also
feel that 'Multi-Column List Partitioning' can be benefited to the Postgres
users in future.

I am attaching the WIP patch for this feature here. It supports
'Multi-Column List Partitioning', however some tasks are still pending. I
would like to know your thoughts about this, So that I can continue the
work with improvising the current patch.

Following things are handled in the patch.
1. Syntax

CREATE TABLE table_name (attrs) PARTITION BY LIST(list_of_columns);

Earlier there was no provision to mention multiple columns as part of the
'list_of_columns' clause. Now we can mention the list of columns separated
by comma.

CREATE TABLE table_name_p1 PARTITION OF table_name FOR VALUES IN
list_of_values.

Whereas list_of_columns can be
a. (value [,...])
b. (value [,...]) [,...]

I would like to list a few examples here for better understanding.
Ex-1:
CREATE TABLE t1(a int) PARTITION BY LIST(a);
CREATE TABLE t1_1 PARTITION OF t1 FOR VALUES IN (1, 2, 10, 5, 7);

Ex-2:
CREATE TABLE t2(a int, b int) PARTITION BY LIST(a,b);
CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES IN (1, 2), (1, 5), (2,
2),(2, 10);

Please share if any changes are required in the above syntax.

2. Modified transformation logic to support above syntax.

3. Modified the data structures to store the information caused by above
syntax. Also modified the searching logic to route the tuple to the
appropriate partition.

4. Done a few basic testing and verified CREATE TABLE, INSERT INTO and
SELECT are working fine.

Following items are pending and I am working on it.

1. Handling of 'NULL' values.

2. Support multi column case in partition pruning.

3. Add test cases to the regression test suite.

Please share your thoughts.

Thanks & Regards,
Nitin Jadhav

#4Amit Langote
amitlangote09@gmail.com
In reply to: Nitin Jadhav (#1)
Re: Multi-Column List Partitioning

Hello Nitin,

On Thu, May 6, 2021 at 11:03 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

Hi,

While reviewing one of the 'Table partitioning' related patches, I found that Postgres does not support multiple column based LIST partitioning. Based on this understanding, I have started working on this feature. I also feel that 'Multi-Column List Partitioning' can be benefited to the Postgres users in future.

Yes, it would be nice to have this. Thanks for picking this up.

I am attaching the WIP patch for this feature here. It supports 'Multi-Column List Partitioning', however some tasks are still pending. I would like to know your thoughts about this, So that I can continue the work with improvising the current patch.

Following things are handled in the patch.
1. Syntax

CREATE TABLE table_name (attrs) PARTITION BY LIST(list_of_columns);

Earlier there was no provision to mention multiple columns as part of the 'list_of_columns' clause. Now we can mention the list of columns separated by comma.

CREATE TABLE table_name_p1 PARTITION OF table_name FOR VALUES IN list_of_values.

Whereas list_of_columns can be
a. (value [,...])
b. (value [,...]) [,...]

I would like to list a few examples here for better understanding.
Ex-1:
CREATE TABLE t1(a int) PARTITION BY LIST(a);
CREATE TABLE t1_1 PARTITION OF t1 FOR VALUES IN (1, 2, 10, 5, 7);

Ex-2:
CREATE TABLE t2(a int, b int) PARTITION BY LIST(a,b);
CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES IN (1, 2), (1, 5), (2, 2),(2, 10);

Hmm, why not have parentheses around these lists, that is: (
(list_of_values) [, ...] )

So your example would look like this:

CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES IN ((1, 2), (1, 5), (2,
2), (2, 10));

IMO, it is not such a bad syntax from a user's PoV. It's not hard to
understand from this syntax that the partition constraint is something
like (a, b) = (1, 2) OR (a, b) = (1, 5) OR ..., where the = performs
row-wise comparison.

I will now take a look at the patch itself.

--
Amit Langote
EDB: http://www.enterprisedb.com

#5Amit Langote
amitlangote09@gmail.com
In reply to: Amit Langote (#4)
Re: Multi-Column List Partitioning

On Fri, May 21, 2021 at 1:02 PM Amit Langote <amitlangote09@gmail.com> wrote:

I will now take a look at the patch itself.

Some quick observations:

* I get a lot of instances of the following 2 warnings when compiling
the patched code:

Warning #1:

partprune.c: In function ‘get_matching_list_bounds’:
partprune.c:2731:11: warning: passing argument 5 of
‘partition_list_bsearch’ makes pointer from integer without a cast
[enabled by default]
nvalues, value, &is_equal);
^
In file included from partprune.c:53:0:
../../../src/include/partitioning/partbounds.h:117:12: note: expected
‘Datum *’ but argument is of type ‘Datum’
extern int partition_list_bsearch(FmgrInfo *partsupfunc,

Warning #2:

partprune.c:2781:12: warning: incompatible integer to pointer
conversion passing 'Datum'
(aka 'unsigned long') to parameter of type 'Datum *' (aka
'unsigned long *'); take the
address with & [-Wint-conversion]

value, &is_equal);

^~~~~

&
../../../src/include/partitioning/partbounds.h:120:32: note: passing
argument to parameter 'value'
here
...int nvalues, Datum *value, bool *is_equal);

* I think this code:

===
/* 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);
===

belongs in the new function transformPartitionListBounds that you
added, because without doing so, any errors having to do with
partitioning columns other than the first one will report the first
column's name in the error message:

postgres=# create table foo (a bool, b bool) partition by list (a, b);
CREATE TABLE

-- this is fine!
postgres=# create table foo_true_true partition of foo for values in (1, true);
ERROR: specified value cannot be cast to type boolean for column "a"
LINE 1: ...able foo_true_true partition of foo for values in (1, true);

-- not this!
postgres=# create table foo_true_true partition of foo for values in (true, 1);
ERROR: specified value cannot be cast to type boolean for column "a"
LINE 1: ...able foo_true_true partition of foo for values in (true, 1);

* The following prototype of transformPartitionListBounds() means that
all values in a given bound list are analyzed with the first
partitioning column's colname, type, typmod, etc., which is wrong:

+static List *
+transformPartitionListBounds(ParseState *pstate, PartitionBoundSpec *spec,
+                            char *colname, Oid coltype, int32 coltypmod,
+                            Oid partcollation, int partnatts)
+{

An example of wrong behavior because of that:

postgres=# create table foo (a bool, b text) partition by list (a, b);
CREATE TABLE
Time: 3.967 ms
postgres=# create table foo_true_true partition of foo for values in
(true, 'whatever');
ERROR: invalid input syntax for type boolean: "whatever"
LINE 1: ...o_true_true partition of foo for values in (true, 'whatever'...

"whatever" should've been accepted but because it's checked with a's
type, it is wrongly flagged.

Please take a look at how transformPartitionRangeBound() handles this,
especially how it uses the correct partitioning column's info to
analyze the corresponding bound value expression.

I will continue looking next week.

--
Amit Langote
EDB: http://www.enterprisedb.com

#6Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Amit Langote (#5)
Re: Multi-Column List Partitioning

Yes, it would be nice to have this. Thanks for picking this up.

Thanks for confirming.

Some quick observations:

Thanks for providing the comments. I will handle these cases.

Hmm, why not have parentheses around these lists, that is: (
(list_of_values) [, ...] )

So your example would look like this:

CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES IN ((1, 2), (1, 5), (2,
2), (2, 10));

I am ok with this syntax. This would be more appropriate.

IMO, it is not such a bad syntax from a user's PoV. It's not hard to
understand from this syntax that the partition constraint is something
like (a, b) = (1, 2) OR (a, b) = (1, 5) OR ..., where the = performs
row-wise comparison.

Thanks for suggesting to use row-wise comparison. I have few queries
with respect to handling of NULL values.

1. What should be the partition constraint for the above case. AFAIK,
row-wise comparison wont work with NULL values as shown in [1]postgres@15890=#SELECT ROW(1, 2) = ROW(1, 2); ?column? ---------- t (1 row). I mean
two rows are considered equal if all their corresponding members are
non-null and equal. The rows are unequal if any corresponding members
are non-null and unequal. Otherwise the result of the row comparison
is unknown (null). So we should generate different types of
constraints for NULL values.

Ex:
CREATE TABLE t(a int, b int) PARTITION BY LIST(a,b);
CREATE TABLE t_1 PARTITION OF t FOR VALUES IN (1, 1), (1, NULL),
(NULL, 1), (NULL, NULL);

As per my knowledge, we should consider creating partition constraints
for the above example as given below.

(a, b) = (1, 1) OR ((a = 1) AND (b IS NULL)) OR ((a IS NULL) AND (b =
1)) OR ((a is NULL) AND (b is NULL)).

Kindly correct me if I am wrong.

2. In the current code we don't put the NULL value in the 'datums'
field of 'PartitionBoundInfoData' structure [2]typedef struct PartitionBoundInfoData { char strategy; /* hash, list or range? */ int ndatums; /* Length of the datums[] array */ Datum **datums; PartitionRangeDatumKind **kind; /* The kind of each range bound datum; * NULL for hash and 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;. Since there can be
only one NULL value, we directly store the corresponding index value
in the 'null_index' field. Now we have to handle multiple NULL values
in case of Multi-Column List Partitioning. So the question is how to
handle this scenario. Following are the 2 approaches to handle this.

Approach-1:
Add another field 'bool **isnull' in [2]typedef struct PartitionBoundInfoData { char strategy; /* hash, list or range? */ int ndatums; /* Length of the datums[] array */ Datum **datums; PartitionRangeDatumKind **kind; /* The kind of each range bound datum; * NULL for hash and 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; and mark the corresponding
element to TRUE if it has NULL value and the corresponding location in
'datums' contains empty/No value. For example, If a partition bound is
(1, NULL), then

datums[0][0] = 1
datums[0][1]postgres@15890=#SELECT ROW(1, 2) = ROW(1, 2); ?column? ---------- t (1 row) = Not assigned any value
isnull[0][0] = FALSE
is null[0][1]postgres@15890=#SELECT ROW(1, 2) = ROW(1, 2); ?column? ---------- t (1 row) = TRUE

So now we have an entry in the 'datums' field for a bound containing
NULL value, so we should handle this in all the scenarios where we are
manipulating 'datums' in order to support NULL values and avoid crash.

Approach-2:
Don't add the bound information to 'datums' field of [2]typedef struct PartitionBoundInfoData { char strategy; /* hash, list or range? */ int ndatums; /* Length of the datums[] array */ Datum **datums; PartitionRangeDatumKind **kind; /* The kind of each range bound datum; * NULL for hash and 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; if any of the
value is NULL. Store this information separately in the structures
mentioned in [3]typedef struct NullBoundDatumInfo { Datum *datum; int col_index; int. bound_index; } NullBoundDatumInfo; and process accordingly.

I feel approach-1 is the better solution as this requires less code
changes and easy to implement than approach-2. Kindly share your
thoughts about the approaches and please share if you have any better
solution than the above 2.

[1]: postgres@15890=#SELECT ROW(1, 2) = ROW(1, 2); ?column? ---------- t (1 row)
postgres@15890=#SELECT ROW(1, 2) = ROW(1, 2);
?column?
----------
t
(1 row)

postgres@15890=#SELECT ROW(1, 2) = ROW(1, 1);
?column?
----------
f
(1 row)

postgres@15890=#SELECT ROW(1, NULL) = ROW(1, NULL);
?column?
----------

(1 row)

postgres@15890=#SELECT ROW(1, 2) = ROW(1, NULL);
?column?
----------

(1 row)

[2]: typedef struct PartitionBoundInfoData { char strategy; /* hash, list or range? */ int ndatums; /* Length of the datums[] array */ Datum **datums; PartitionRangeDatumKind **kind; /* The kind of each range bound datum; * NULL for hash and 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;
typedef struct PartitionBoundInfoData
{
char strategy; /* hash, list or range? */
int ndatums; /* Length of the datums[] array */
Datum **datums;
PartitionRangeDatumKind **kind; /* The kind of each range bound datum;
* NULL for hash and 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;

[3]: typedef struct NullBoundDatumInfo { Datum *datum; int col_index; int. bound_index; } NullBoundDatumInfo;
typedef struct NullBoundDatumInfo
{
Datum *datum;
int col_index;
int. bound_index;
} NullBoundDatumInfo;

typedef struct NullBoundIsNullInfo
{
int col_index;
int. bound_index;
} NullBoundIsNullInfo;

Add 2 fields of type 'NullBoundDatumInfo' and 'NullBoundIsNullInfo' to
the structure [2]typedef struct PartitionBoundInfoData { char strategy; /* hash, list or range? */ int ndatums; /* Length of the datums[] array */ Datum **datums; PartitionRangeDatumKind **kind; /* The kind of each range bound datum; * NULL for hash and 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;.

--
Thanks & Regards,
Nitin Jadhav

Show quoted text

On Fri, May 21, 2021 at 5:47 PM Amit Langote <amitlangote09@gmail.com> wrote:

On Fri, May 21, 2021 at 1:02 PM Amit Langote <amitlangote09@gmail.com> wrote:

I will now take a look at the patch itself.

Some quick observations:

* I get a lot of instances of the following 2 warnings when compiling
the patched code:

Warning #1:

partprune.c: In function ‘get_matching_list_bounds’:
partprune.c:2731:11: warning: passing argument 5 of
‘partition_list_bsearch’ makes pointer from integer without a cast
[enabled by default]
nvalues, value, &is_equal);
^
In file included from partprune.c:53:0:
../../../src/include/partitioning/partbounds.h:117:12: note: expected
‘Datum *’ but argument is of type ‘Datum’
extern int partition_list_bsearch(FmgrInfo *partsupfunc,

Warning #2:

partprune.c:2781:12: warning: incompatible integer to pointer
conversion passing 'Datum'
(aka 'unsigned long') to parameter of type 'Datum *' (aka
'unsigned long *'); take the
address with & [-Wint-conversion]

value, &is_equal);

^~~~~

&
../../../src/include/partitioning/partbounds.h:120:32: note: passing
argument to parameter 'value'
here
...int nvalues, Datum *value, bool *is_equal);

* I think this code:

===
/* 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);
===

belongs in the new function transformPartitionListBounds that you
added, because without doing so, any errors having to do with
partitioning columns other than the first one will report the first
column's name in the error message:

postgres=# create table foo (a bool, b bool) partition by list (a, b);
CREATE TABLE

-- this is fine!
postgres=# create table foo_true_true partition of foo for values in (1, true);
ERROR: specified value cannot be cast to type boolean for column "a"
LINE 1: ...able foo_true_true partition of foo for values in (1, true);

-- not this!
postgres=# create table foo_true_true partition of foo for values in (true, 1);
ERROR: specified value cannot be cast to type boolean for column "a"
LINE 1: ...able foo_true_true partition of foo for values in (true, 1);

* The following prototype of transformPartitionListBounds() means that
all values in a given bound list are analyzed with the first
partitioning column's colname, type, typmod, etc., which is wrong:

+static List *
+transformPartitionListBounds(ParseState *pstate, PartitionBoundSpec *spec,
+                            char *colname, Oid coltype, int32 coltypmod,
+                            Oid partcollation, int partnatts)
+{

An example of wrong behavior because of that:

postgres=# create table foo (a bool, b text) partition by list (a, b);
CREATE TABLE
Time: 3.967 ms
postgres=# create table foo_true_true partition of foo for values in
(true, 'whatever');
ERROR: invalid input syntax for type boolean: "whatever"
LINE 1: ...o_true_true partition of foo for values in (true, 'whatever'...

"whatever" should've been accepted but because it's checked with a's
type, it is wrongly flagged.

Please take a look at how transformPartitionRangeBound() handles this,
especially how it uses the correct partitioning column's info to
analyze the corresponding bound value expression.

I will continue looking next week.

--
Amit Langote
EDB: http://www.enterprisedb.com

#7Amit Langote
amitlangote09@gmail.com
In reply to: Nitin Jadhav (#6)
Re: Multi-Column List Partitioning

On Sun, May 23, 2021 at 6:49 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

IMO, it is not such a bad syntax from a user's PoV. It's not hard to
understand from this syntax that the partition constraint is something
like (a, b) = (1, 2) OR (a, b) = (1, 5) OR ..., where the = performs
row-wise comparison.

Thanks for suggesting to use row-wise comparison.

Actually, I was just describing how the *users* may want to visualize
the partition constraint...

I have few queries
with respect to handling of NULL values.

1. What should be the partition constraint for the above case. AFAIK,
row-wise comparison wont work with NULL values as shown in [1]. I mean
two rows are considered equal if all their corresponding members are
non-null and equal. The rows are unequal if any corresponding members
are non-null and unequal. Otherwise the result of the row comparison
is unknown (null). So we should generate different types of
constraints for NULL values.

Ex:
CREATE TABLE t(a int, b int) PARTITION BY LIST(a,b);
CREATE TABLE t_1 PARTITION OF t FOR VALUES IN (1, 1), (1, NULL),
(NULL, 1), (NULL, NULL);

As per my knowledge, we should consider creating partition constraints
for the above example as given below.

(a, b) = (1, 1) OR ((a = 1) AND (b IS NULL)) OR ((a IS NULL) AND (b =
1)) OR ((a is NULL) AND (b is NULL)).

Yeah, something like that should do the trick.

Again, I was not actually suggesting that you write code to implement
the constraint using something like RowCompareExpr, only that the
users might want to view the constraint as doing row-wise comparison
of the partitioning columns and the specified value lists.

2. In the current code we don't put the NULL value in the 'datums'
field of 'PartitionBoundInfoData' structure [2]. Since there can be
only one NULL value, we directly store the corresponding index value
in the 'null_index' field. Now we have to handle multiple NULL values
in case of Multi-Column List Partitioning. So the question is how to
handle this scenario. Following are the 2 approaches to handle this.

Approach-1:
Add another field 'bool **isnull' in [2] and mark the corresponding
element to TRUE if it has NULL value and the corresponding location in
'datums' contains empty/No value. For example, If a partition bound is
(1, NULL), then

datums[0][0] = 1
datums[0][1] = Not assigned any value
isnull[0][0] = FALSE
is null[0][1] = TRUE

So now we have an entry in the 'datums' field for a bound containing
NULL value, so we should handle this in all the scenarios where we are
manipulating 'datums' in order to support NULL values and avoid crash.

Approach-2:
Don't add the bound information to 'datums' field of [2] if any of the
value is NULL. Store this information separately in the structures
mentioned in [3] and process accordingly.

I feel approach-1 is the better solution as this requires less code
changes and easy to implement than approach-2. Kindly share your
thoughts about the approaches and please share if you have any better
solution than the above 2.

Approach 1 sounds better. It sounds like approach 1 might help us
implement support for allowing NULLs in range partition bounds in the
future, if at all. For now, it might be better to not allocate the
isnull array except for list partitioning.

I'll wait for you to post a new patch addressing at least the comments
in my earlier email. Also, please make sure to run `make check`
successfully before posting the patch. :)

Thanks.

--
Amit Langote
EDB: http://www.enterprisedb.com

#8Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Amit Langote (#7)
1 attachment(s)
Re: Multi-Column List Partitioning

Approach 1 sounds better. It sounds like approach 1 might help us
implement support for allowing NULLs in range partition bounds in the
future, if at all. For now, it might be better to not allocate the
isnull array except for list partitioning.

Thanks for confirming.

I'll wait for you to post a new patch addressing at least the comments
in my earlier email. Also, please make sure to run `make check`
successfully before posting the patch. :)

I have fixed all of the review comments given by you and Jeevan in the
attached patch and also the attached patch contains more changes
compared to the previous patch. Following are the implementation
details.

1. Regarding syntax, the existing syntax will work fine for the
single-column list partitioning. However I have used the new syntax
for the multi-column list partitioning as we discussed earlier. I have
used a combination of 'AND' and 'OR' logic for the partition
constraints as given in the below example.

postgres@17503=#create table t(a int, b text) partition by list(a,b);
CREATE TABLE
postgres@17503=#create table t1 partition of t for values in ((1,'a'),
(NULL,'b'));
CREATE TABLE
postgres@17503=#\d+ t
Partitioned table "public.t"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+----------+-------------+--------------+-------------
a | integer | | | | plain |
| |
b | text | | | | extended |
| |
Partition key: LIST (a, b)
Partitions: t1 FOR VALUES IN ((1, 'a'), (NULL, 'b'))

postgres@17503=#\d+ t1
Table "public.t1"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+----------+-------------+--------------+-------------
a | integer | | | | plain |
| |
b | text | | | | extended |
| |
Partition of: t FOR VALUES IN ((1, 'a'), (NULL, 'b'))
Partition constraint: (((a = 1) AND (b = 'a'::text)) OR ((a IS NULL)
AND (b = 'b'::text)))
Access method: heap

2. In the existing code, NULL values were handled differently. It was
not added to the 'datums' variable, rather used to store the partition
index directly in the 'null_index' variable. Now there is a
possibility of multiple NULL values, hence introducing a new member
'isnulls' in the 'PartitionBoundInfoData' struct which indicates
whether the corresponding element in the 'datums' is NULL. Now
'null_index' cannot be used directly to store the partition index, so
removed it and made the necessary changes in multiple places.

3. I have added test cases for 'create table' and 'insert' statements
related to multi-column list partitioning and these are working fine
with 'make check'.

4. Handled the partition pruning code to accommodate these changes for
single-column list partitioning. However it is pending for
multi-column list partitioning.

5. I have done necessary changes in partition wise join related code
to accommodate for single-column list partitioning. However it is
pending for multi-column list partitioning.

Kindly review the patch and let me know if any changes are required.

Pending items:
1. Support of partition pruning for multi-column list partitioning.
2. Support of partition wise join for multi-column list partitioning.

I will continue to work on the above 2 items.
Kindly let me know if I am missing something.

Thanks & Regards,
Nitin Jadhav

Show quoted text

On Wed, May 26, 2021 at 10:27 AM Amit Langote <amitlangote09@gmail.com> wrote:

On Sun, May 23, 2021 at 6:49 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

IMO, it is not such a bad syntax from a user's PoV. It's not hard to
understand from this syntax that the partition constraint is something
like (a, b) = (1, 2) OR (a, b) = (1, 5) OR ..., where the = performs
row-wise comparison.

Thanks for suggesting to use row-wise comparison.

Actually, I was just describing how the *users* may want to visualize
the partition constraint...

I have few queries
with respect to handling of NULL values.

1. What should be the partition constraint for the above case. AFAIK,
row-wise comparison wont work with NULL values as shown in [1]. I mean
two rows are considered equal if all their corresponding members are
non-null and equal. The rows are unequal if any corresponding members
are non-null and unequal. Otherwise the result of the row comparison
is unknown (null). So we should generate different types of
constraints for NULL values.

Ex:
CREATE TABLE t(a int, b int) PARTITION BY LIST(a,b);
CREATE TABLE t_1 PARTITION OF t FOR VALUES IN (1, 1), (1, NULL),
(NULL, 1), (NULL, NULL);

As per my knowledge, we should consider creating partition constraints
for the above example as given below.

(a, b) = (1, 1) OR ((a = 1) AND (b IS NULL)) OR ((a IS NULL) AND (b =
1)) OR ((a is NULL) AND (b is NULL)).

Yeah, something like that should do the trick.

Again, I was not actually suggesting that you write code to implement
the constraint using something like RowCompareExpr, only that the
users might want to view the constraint as doing row-wise comparison
of the partitioning columns and the specified value lists.

2. In the current code we don't put the NULL value in the 'datums'
field of 'PartitionBoundInfoData' structure [2]. Since there can be
only one NULL value, we directly store the corresponding index value
in the 'null_index' field. Now we have to handle multiple NULL values
in case of Multi-Column List Partitioning. So the question is how to
handle this scenario. Following are the 2 approaches to handle this.

Approach-1:
Add another field 'bool **isnull' in [2] and mark the corresponding
element to TRUE if it has NULL value and the corresponding location in
'datums' contains empty/No value. For example, If a partition bound is
(1, NULL), then

datums[0][0] = 1
datums[0][1] = Not assigned any value
isnull[0][0] = FALSE
is null[0][1] = TRUE

So now we have an entry in the 'datums' field for a bound containing
NULL value, so we should handle this in all the scenarios where we are
manipulating 'datums' in order to support NULL values and avoid crash.

Approach-2:
Don't add the bound information to 'datums' field of [2] if any of the
value is NULL. Store this information separately in the structures
mentioned in [3] and process accordingly.

I feel approach-1 is the better solution as this requires less code
changes and easy to implement than approach-2. Kindly share your
thoughts about the approaches and please share if you have any better
solution than the above 2.

Approach 1 sounds better. It sounds like approach 1 might help us
implement support for allowing NULLs in range partition bounds in the
future, if at all. For now, it might be better to not allocate the
isnull array except for list partitioning.

I'll wait for you to post a new patch addressing at least the comments
in my earlier email. Also, please make sure to run `make check`
successfully before posting the patch. :)

Thanks.

--
Amit Langote
EDB: http://www.enterprisedb.com

Attachments:

v1_multi_column_list_partitioning.patchapplication/octet-stream; name=v1_multi_column_list_partitioning.patchDownload
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 028e8ac..28fd15d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16553,13 +16553,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 606c920..218054a 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -1265,19 +1265,15 @@ 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);
+													  key->partnatts,
+													  values, isnull,
+													  &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 d5b67d4..d3ece78 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -146,6 +146,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);
 
 
 /*
@@ -4017,6 +4020,42 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 }
 
 /*
+ * checkForDuplicates
+ *
+ * Return TRUE if the list bound element is already present in the list of list
+ * bounds, FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)
+{
+	ListCell   *cell = NULL;
+
+	foreach(cell, source)
+	{
+		int		i = 0;
+		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(searchElem, i));
+
+			if (!equal(value1, value2))
+			{
+				isDuplicate = false;
+				break;
+			}
+		}
+
+		if (isDuplicate)
+			return true;
+	}
+
+	return false;
+}
+
+/*
  * transformPartitionBound
  *
  * Transform a partition bound specification
@@ -4029,7 +4068,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);
@@ -4079,62 +4117,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 = castNode(Const, lfirst(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)
 	{
@@ -4171,6 +4161,111 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 }
 
 /*
+ * transformPartitionListBounds
+ * 		This functions converts the expressions of list partition bounds
+ * 		from the raw grammar representation.
+ */
+static List *
+transformPartitionListBounds(ParseState *pstate, PartitionBoundSpec *spec,
+							 Relation parent)
+{
+	int				i = 0;
+	int				j = 0;
+	ListCell	   *cell = NULL;
+	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] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+			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;
+		bool		isDuplicate = false;
+
+		if (partnatts == 1)
+		{
+			Const	   *val =
+				transformPartitionBoundValue(pstate, expr,colname[0],
+											 coltype[0], coltypmod[0],
+											 partcollation[0]);
+			values = lappend(values, val);
+		}
+		else
+		{
+			ListCell   *cell2 = NULL;
+			RowExpr		*rowexpr = NULL;
+
+			if (!IsA(expr, RowExpr))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("Invalid list bound specification"),
+						parser_errposition(pstate, exprLocation((Node *) spec))));
+
+			rowexpr = (RowExpr *) expr;
+			if (partnatts != list_length(rowexpr->args))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("Must specify exactly one value per partitioning column"),
+						 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 */
+		isDuplicate = checkForDuplicates(result, values);
+		if (isDuplicate)
+			continue;
+
+		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 7925fcc..bb6cddc 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -53,11 +53,12 @@ typedef struct PartitionHashBound
 	int			index;
 } PartitionHashBound;
 
-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
 typedef struct PartitionListValue
 {
 	int			index;
-	Datum		value;
+	Datum	   *values;
+	bool	   *isnulls;
 } PartitionListValue;
 
 /* One bound of a range partition */
@@ -175,6 +176,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,
@@ -230,6 +232,7 @@ static Oid	get_partition_operator(PartitionKey key, int col,
 								   StrategyNumber strategy, bool *need_relabel);
 static List *get_qual_for_hash(Relation parent, PartitionBoundSpec *spec);
 static List *get_qual_for_list(Relation parent, PartitionBoundSpec *spec);
+static List *get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec);
 static List *get_qual_for_range(Relation parent, PartitionBoundSpec *spec,
 								bool for_default);
 static void get_range_key_properties(PartitionKey key, int keynum,
@@ -367,8 +370,8 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
 	/* No special hash partitions. */
-	boundinfo->null_index = -1;
 	boundinfo->default_index = -1;
+	boundinfo->isnulls = NULL;
 
 	ndatums = nparts;
 	hbounds = (PartitionHashBound **)
@@ -433,6 +436,56 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 }
 
 /*
+ * partition_bound_accepts_nulls
+ * 		Returns TRUE if partition bound has NULL value, FALSE otherwise.
+ */
+bool partition_bound_accepts_nulls(PartitionBoundInfo boundinfo)
+{
+	int i = 0;
+	int j = 0;
+
+	if (!boundinfo->isnulls)
+		return false;
+
+	for (i = 0; i < boundinfo->ndatums; i++)
+	{
+		//TODO: Handle for multi-column cases
+		for (j = 0; j < 1; j++)
+		{
+			if (boundinfo->isnulls[i][j])
+				return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * get_partition_bound_null_index
+ * 		Returns the partition index of the partition bound which accepts NULL.
+ */
+int get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+{
+	int i = 0;
+	int j = 0;
+
+	if (!boundinfo->isnulls)
+		return -1;
+
+	for (i = 0; i < boundinfo->ndatums; i++)
+	{
+		//TODO: Handle for multi-column cases
+		for (j = 0; j < 1; j++)
+		{
+			if (boundinfo->isnulls[i][j])
+				return boundinfo->indexes[i];
+		}
+	}
+
+	return -1;
+}
+
+/*
  * create_list_bounds
  *		Create a PartitionBoundInfo for a list partitioned table
  */
@@ -447,14 +500,12 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	int			ndatums = 0;
 	int			next_index = 0;
 	int			default_index = -1;
-	int			null_index = -1;
 	List	   *non_null_values = NIL;
 
 	boundinfo = (PartitionBoundInfoData *)
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
 	/* Will be set correctly below. */
-	boundinfo->null_index = -1;
 	boundinfo->default_index = -1;
 
 	/* Create a unified list of non-null values across all partitions. */
@@ -479,29 +530,30 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 
 		foreach(c, spec->listdatums)
 		{
-			Const	   *val = castNode(Const, lfirst(c));
+			int					j = 0;
+			List			   *elem = lfirst(c);
+			ListCell		   *cell = NULL;
 			PartitionListValue *list_value = NULL;
 
-			if (!val->constisnull)
-			{
-				list_value = (PartitionListValue *)
-					palloc0(sizeof(PartitionListValue));
-				list_value->index = i;
-				list_value->value = val->constvalue;
-			}
-			else
+			list_value = (PartitionListValue *)
+				palloc0(sizeof(PartitionListValue));
+			list_value->index = i;
+			list_value->values = (Datum *) palloc0(key->partnatts * sizeof(Datum));
+			list_value->isnulls = (bool *) palloc0(key->partnatts * sizeof(bool));
+
+			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 = castNode(Const, lfirst(cell));
+
+				if (!val->constisnull)
+					list_value->values[j] = val->constvalue;
+				else
+					list_value->isnulls[j] = true;
+
+				j++;
 			}
 
-			if (list_value)
-				non_null_values = lappend(non_null_values, list_value);
+			non_null_values = lappend(non_null_values, list_value);
 		}
 	}
 
@@ -520,7 +572,8 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 
 		all_values[i] = (PartitionListValue *)
 			palloc(sizeof(PartitionListValue));
-		all_values[i]->value = src->value;
+		all_values[i]->values = src->values;
+		all_values[i]->isnulls= src->isnulls;
 		all_values[i]->index = src->index;
 		i++;
 	}
@@ -530,6 +583,7 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 
 	boundinfo->ndatums = ndatums;
 	boundinfo->datums = (Datum **) palloc0(ndatums * sizeof(Datum *));
+	boundinfo->isnulls = (bool **) palloc0(ndatums * sizeof(bool *));
 	boundinfo->nindexes = ndatums;
 	boundinfo->indexes = (int *) palloc(ndatums * sizeof(int));
 
@@ -541,12 +595,19 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	 */
 	for (i = 0; i < ndatums; i++)
 	{
+		int			j = 0;
 		int			orig_index = all_values[i]->index;
+		boundinfo->datums[i] = (Datum *) palloc(key->partnatts * sizeof(Datum));
+		boundinfo->isnulls[i] = (bool *) palloc(key->partnatts * sizeof(bool));
 
-		boundinfo->datums[i] = (Datum *) palloc(sizeof(Datum));
-		boundinfo->datums[i][0] = datumCopy(all_values[i]->value,
-											key->parttypbyval[0],
-											key->parttyplen[0]);
+		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)
@@ -555,22 +616,6 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 		boundinfo->indexes[i] = (*mapping)[orig_index];
 	}
 
-	/*
-	 * 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)
 	{
@@ -611,10 +656,9 @@ 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;
 	/* Will be set correctly below. */
 	boundinfo->default_index = -1;
+	boundinfo->isnulls = NULL;
 
 	all_bounds = (PartitionRangeBound **)
 		palloc0(2 * nparts * sizeof(PartitionRangeBound *));
@@ -802,6 +846,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;
@@ -812,7 +858,7 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 	if (b1->nindexes != b2->nindexes)
 		return false;
 
-	if (b1->null_index != b2->null_index)
+	if (get_partition_bound_null_index(b1) != get_partition_bound_null_index(b2))
 		return false;
 
 	if (b1->default_index != b2->default_index)
@@ -885,7 +931,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. Dont compare 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;
 			}
@@ -922,10 +983,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)
 	{
@@ -955,6 +1017,8 @@ partition_bounds_copy(PartitionBoundInfo src,
 		int			j;
 
 		dest->datums[i] = (Datum *) palloc(sizeof(Datum) * natts);
+		if (src->isnulls)
+			dest->isnulls[i] = (bool *) palloc(sizeof(bool) * natts);
 
 		for (j = 0; j < natts; j++)
 		{
@@ -972,17 +1036,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[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;
@@ -1100,6 +1169,8 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 	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);
+	int			outer_null_index = get_partition_bound_null_index(outer_bi);
+	int			inner_null_index = get_partition_bound_null_index(inner_bi);
 	PartitionMap outer_map;
 	PartitionMap inner_map;
 	int			outer_pos;
@@ -1109,6 +1180,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);
@@ -1146,6 +1218,35 @@ 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 		is_all_match = false;
+
+		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];
+
+		//TODO: Handle for multi-column case.
+		if (outer_isnull[0] && inner_isnull[0])
+		{
+			outer_pos++;
+			inner_pos++;
+			continue;
+		}
+		else if (outer_isnull[0])
+		{
+			outer_pos++;
+			continue;
+		}
+		else if (inner_isnull[0])
+		{
+			inner_pos++;
+			continue;
+		}
+
 
 		if (outer_pos < outer_bi->ndatums)
 		{
@@ -1196,13 +1297,15 @@ 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]));
+			//TODO: handle multi-column case
+			cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation, 1,  //TODO: get attr count
+												outer_datums,
+												outer_isnull,
+												inner_datums,
+												inner_isnull, &is_all_match);
 		}
 
-		if (cmpval == 0)
+		if (is_all_match)
 		{
 			/* Two list values match exactly. */
 			Assert(outer_pos < outer_bi->ndatums);
@@ -1221,6 +1324,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				goto cleanup;
 
 			merged_datum = outer_datums;
+			merged_isnull = outer_isnull;
 
 			/* Move to the next pair of list values. */
 			outer_pos++;
@@ -1254,6 +1358,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				if (merged_index == -1)
 					goto cleanup;
 				merged_datum = outer_datums;
+				merged_isnull = outer_isnull;
 			}
 
 			/* Move to the next list value on the outer side. */
@@ -1288,6 +1393,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				if (merged_index == -1)
 					goto cleanup;
 				merged_datum = inner_datums;
+				merged_isnull = inner_isnull;
 			}
 
 			/* Move to the next list value on the inner side. */
@@ -1302,6 +1408,7 @@ 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);
 		}
 	}
 
@@ -1310,17 +1417,17 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 	 * non-existent.
 	 */
 	if (outer_has_null &&
-		is_dummy_partition(outer_rel, outer_bi->null_index))
+		is_dummy_partition(outer_rel, outer_null_index))
 		outer_has_null = false;
 	if (inner_has_null &&
-		is_dummy_partition(inner_rel, inner_bi->null_index))
+		is_dummy_partition(inner_rel, inner_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,
+							  outer_null_index, inner_null_index,
 							  jointype, &next_index, &null_index);
 	else
 		Assert(null_index == -1);
@@ -1358,6 +1465,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,
@@ -1368,6 +1476,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);
@@ -1676,6 +1785,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,
@@ -2407,19 +2517,41 @@ 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);
 	int			pos;
 	ListCell   *lc;
+	int			natts = 1;  //TODO: Handle for multi-column case
+	bool	   *null = NULL;
 
 	merged_bounds = (PartitionBoundInfo) palloc(sizeof(PartitionBoundInfoData));
 	merged_bounds->strategy = strategy;
-	merged_bounds->ndatums = ndatums;
 
+	if (merged_isnulls)
+	{
+		if (null_index >= 0)
+		{
+			null = (bool *) palloc0(sizeof(bool) * natts);
+			null[0] = true;
+			ndatums++;
+		}
+		merged_bounds->isnulls = (bool **) palloc(sizeof(bool *) * ndatums);
+
+		pos = 0;
+		foreach(lc, merged_isnulls)
+			merged_bounds->isnulls[pos++] = (bool *) lfirst(lc);
+
+		if (null_index >= 0)
+			merged_bounds->isnulls[pos] = null;
+	}
+
+	merged_bounds->ndatums = ndatums;
 	merged_bounds->datums = (Datum **) palloc(sizeof(Datum *) * ndatums);
+
 	pos = 0;
 	foreach(lc, merged_datums)
 		merged_bounds->datums[pos++] = (Datum *) lfirst(lc);
@@ -2436,6 +2568,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
 	{
@@ -2444,14 +2577,17 @@ build_merged_partition_bounds(char strategy, List *merged_datums,
 		merged_bounds->kind = 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;
+	if (merged_isnulls && null_index >= 0)
+		merged_bounds->indexes[pos] = null_index;
+
 	merged_bounds->default_index = default_index;
 
 	return merged_bounds;
@@ -2953,32 +3089,37 @@ check_new_partition_bound(char *relname, Relation parent,
 
 					foreach(cell, spec->listdatums)
 					{
-						Const	   *val = castNode(Const, lfirst(cell));
-
-						overlap_location = val->location;
-						if (!val->constisnull)
+						int			i = 0;
+						int         offset = -1;
+						bool        equal = false;
+						List	   *elem = lfirst(cell);
+						Datum	   *values = (Datum *) palloc0(key->partnatts * sizeof(Datum));
+						bool	   *isnulls = (bool *) palloc0(key->partnatts * sizeof(bool));
+
+						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,
+														key->partnatts,
+														values, isnulls,
+														&equal);
+						if (offset >= 0 && equal)
 						{
 							overlap = true;
-							with = boundinfo->null_index;
+							with = boundinfo->indexes[offset];
 							break;
 						}
+
+						pfree(values);
+						pfree(isnulls);
 					}
 				}
 
@@ -3491,6 +3632,53 @@ partition_hbound_cmp(int modulus1, int remainder1, int modulus2, int remainder2)
 }
 
 /*
+ * partition_lbound_datum_cmp
+ *
+ * This function compares the list bound values of all the partition key
+ * columns. Returns the value of 'cmpval' if the first bound value does
+ * not match, otherwise returns zero. If it successfully compares the bound
+ * values for all the partition key, then it sets is_all_match to TRUE.
+ */
+int32
+partition_lbound_datum_cmp(FmgrInfo *partsupfunc, Oid *partcollation,
+						   int nvalues, Datum *lb_datums, bool *lb_isnulls,
+						   Datum *values, bool *isnulls, bool *is_all_match)
+{
+	int		i = 0;
+	int32	cmpval = 0;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		bool isnull = false;
+
+		if (isnulls)
+			isnull = isnulls[i];
+
+		if (lb_isnulls[i] && isnull)
+			cmpval = 0;
+		else if (lb_isnulls[i])
+			cmpval = 1;
+		else if (isnull)
+			cmpval = -1;
+		else
+			cmpval = DatumGetInt32(FunctionCall2Coll(&partsupfunc[i],
+													 partcollation[i],
+													 lb_datums[i], values[i]));
+
+		if (cmpval != 0)
+			break;
+	}
+
+	if (i == 0)
+		return cmpval;
+
+	if (i == nvalues && cmpval == 0)
+		*is_all_match = true;
+
+	return 0;
+}
+
+/*
  * 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
@@ -3500,8 +3688,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, int nvalues,
+					   Datum *values, bool *isnulls, bool *is_equal)
 {
 	int			lo,
 				hi,
@@ -3512,16 +3700,76 @@ partition_list_bsearch(FmgrInfo *partsupfunc, Oid *partcollation,
 	while (lo < hi)
 	{
 		int32		cmpval;
+		bool		is_all_match = false;
 
 		mid = (lo + hi + 1) / 2;
-		cmpval = DatumGetInt32(FunctionCall2Coll(&partsupfunc[0],
-												 partcollation[0],
-												 boundinfo->datums[mid][0],
-												 value));
+		cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+											nvalues, boundinfo->datums[mid],
+											boundinfo->isnulls[mid], values,
+											isnulls, &is_all_match);
+
+		if (is_all_match)
+		{
+			*is_equal = true;
+			return mid;
+		}
+
+		if (cmpval == 0)
+		{
+			/*
+			 * Once we find the matching for the first column but if it does not
+			 * match for the any of the other columns, then the binary search
+			 * will not work in all the cases. We should traverse just below
+			 * and above the mid index until we find the match or we reach the
+			 * first mismatch.
+			 */
+			int32	off = mid;
+			while (off >= 1)
+			{
+				cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+													nvalues, boundinfo->datums[off - 1],
+													boundinfo->isnulls[off - 1], values,
+													isnulls, &is_all_match);
+
+				if (is_all_match)
+				{
+					/* Found the matching bound. Return the offset. */
+					*is_equal = true;
+					return (off - 1);
+				}
+
+				if (cmpval != 0)
+					break;
+
+				off--;
+			}
+
+			off = mid;
+			while (off < boundinfo->ndatums - 1)
+			{
+				cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+													nvalues, boundinfo->datums[off + 1],
+													boundinfo->isnulls[off + 1], values,
+													isnulls, &is_all_match);
+
+				if (is_all_match)
+				{
+					/* Found the matching bound. Return the offset. */
+					*is_equal = true;
+					return (off + 1);
+				}
+
+				if (cmpval != 0)
+					break;
+
+				off++;
+			}
+		}
+
 		if (cmpval <= 0)
 		{
 			lo = mid;
-			*is_equal = (cmpval == 0);
+			*is_equal = (cmpval == 0 && is_all_match);
 			if (*is_equal)
 				break;
 		}
@@ -3687,13 +3935,23 @@ 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	   *val1 = (*(PartitionListValue *const *) a)->values;
+	Datum	   *val2 = (*(PartitionListValue *const *) b)->values;
+	bool	   *null1 = (*(PartitionListValue *const *) a)->isnulls;
+	bool	   *null2 = (*(PartitionListValue *const *) b)->isnulls;
+
 	PartitionKey key = (PartitionKey) arg;
 
-	return DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[0],
-										   key->partcollation[0],
-										   val1, val2));
+	if (null1[0] && null2[0])
+		return 0;
+	else if (null1[0])
+		return 1;
+	else if (null2[0])
+		return -1;
+	else
+		return DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[0],
+											   key->partcollation[0],
+											   val1[0], val2[0]));
 }
 
 /*
@@ -3793,9 +4051,8 @@ make_partition_op_expr(PartitionKey key, int keynum,
 				int			nelems = list_length(elems);
 
 				Assert(nelems >= 1);
-				Assert(keynum == 0);
 
-				if (nelems > 1 &&
+				if (key->partnatts == 1 && nelems > 1 &&
 					!type_is_array(key->parttypid[keynum]))
 				{
 					ArrayExpr  *arrexpr;
@@ -3820,10 +4077,9 @@ make_partition_op_expr(PartitionKey key, int keynum,
 					saopexpr->inputcollid = key->partcollation[keynum];
 					saopexpr->args = list_make2(arg1, arrexpr);
 					saopexpr->location = -1;
-
 					result = (Expr *) saopexpr;
 				}
-				else
+				else if (key->partnatts == 1)
 				{
 					List	   *elemops = NIL;
 					ListCell   *lc;
@@ -3844,9 +4100,17 @@ make_partition_op_expr(PartitionKey key, int keynum,
 
 					result = nelems > 1 ? makeBoolExpr(OR_EXPR, elemops, -1) : linitial(elemops);
 				}
+				else
+				{
+					result = make_opclause(operoid,
+										   BOOLOID,
+										   false,
+										   arg1, arg2,
+										   InvalidOid,
+										   key->partcollation[keynum]);
+				}
 				break;
 			}
-
 		case PARTITION_STRATEGY_RANGE:
 			result = make_opclause(operoid,
 								   BOOLOID,
@@ -3968,11 +4232,8 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 	List	   *elems = NIL;
 	bool		list_has_null = false;
 
-	/*
-	 * Only single-column list partitioning is supported, so we are worried
-	 * only about the partition key with index 0.
-	 */
-	Assert(key->partnatts == 1);
+	if (key->partnatts > 1)
+		return get_qual_for_multi_column_list(parent, spec);
 
 	/* Construct Var or expression representing the partition column */
 	if (key->partattrs[0] != 0)
@@ -3998,13 +4259,8 @@ 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.
@@ -4016,6 +4272,12 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 		{
 			Const	   *val;
 
+			if (boundinfo->isnulls[i][0])
+			{
+				list_has_null = true;
+				continue;
+			}
+
 			/*
 			 * Construct Const from known-not-null datum.  We must be careful
 			 * to copy the value, because our result has to be able to outlive
@@ -4025,7 +4287,7 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 							key->parttypmod[0],
 							key->parttypcoll[0],
 							key->parttyplen[0],
-							datumCopy(*boundinfo->datums[i],
+							datumCopy(boundinfo->datums[i][0],
 									  key->parttypbyval[0],
 									  key->parttyplen[0]),
 							false,	/* isnull */
@@ -4041,12 +4303,17 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 		 */
 		foreach(cell, spec->listdatums)
 		{
-			Const	   *val = castNode(Const, lfirst(cell));
+			ListCell	   *cell2 = NULL;
 
-			if (val->constisnull)
-				list_has_null = true;
-			else
-				elems = lappend(elems, copyObject(val));
+			foreach(cell2, (List *) lfirst(cell))
+			{
+				Const      *val = castNode(Const, lfirst(cell2));
+
+				if (val->constisnull)
+					list_has_null = true;
+				else
+					elems = lappend(elems, copyObject(val));
+			}
 		}
 	}
 
@@ -4122,6 +4389,158 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 }
 
 /*
+ * get_qual_for_list_for_multi_column
+ *
+ * Returns a list of expressions to use as a list partition's constraint,
+ * given the parent relation and partition bound structure.
+ *
+ * The function returns NIL for a default partition when it's the only
+ * partition since in that case there is no constraint.
+ */
+static List *
+get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
+{
+	int			i = 0;
+	int			j = 0;
+	PartitionKey key = RelationGetPartitionKey(parent);
+	List	   *result;
+	Expr	   *opexpr;
+	NullTest   *nulltest;
+	ListCell   *cell;
+	List	   *elems = NIL;
+	Expr      **keyCol = (Expr **) palloc0 (key->partnatts * sizeof(Expr *));
+
+	/* Construct Var or expression representing the partition columns */
+	for (i = 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;
+		}
+	}
+
+	/*
+	 * For default list partition, collect datums for all the partitions. The
+	 * default partition constraint should check that the partition key is
+	 * equal to none of those.
+	 */
+	if (spec->is_default)
+	{
+		int			ndatums = 0;
+		PartitionDesc pdesc = RelationGetPartitionDesc(parent, false);
+		PartitionBoundInfo boundinfo = pdesc->boundinfo;
+
+		if (boundinfo)
+			ndatums = boundinfo->ndatums;
+
+		/*
+		 * If default is the only partition, there need not be any partition
+		 * constraint on it.
+		 */
+		if (ndatums == 0)
+			return NIL;
+
+		for (i = 0; i < ndatums; i++)
+		{
+			List       *andexpr = NIL;
+
+			for (j = 0; j < key->partnatts; j++)
+			{
+				Const      *val = NULL;
+
+				if (boundinfo->isnulls[i][j])
+				{
+					nulltest = makeNode(NullTest);
+					nulltest->arg = keyCol[j];
+					nulltest->nulltesttype = IS_NULL;
+					nulltest->argisrow = false;
+					nulltest->location = -1;
+					andexpr = lappend(andexpr, 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]);
+
+					opexpr = make_partition_op_expr(key, j, BTEqualStrategyNumber,
+													keyCol[j], (Expr *) val);
+					andexpr = lappend(andexpr, opexpr);
+				}
+			}
+
+			opexpr = makeBoolExpr(AND_EXPR, andexpr, -1);
+			elems = lappend(elems, opexpr);
+		}
+	}
+	else
+	{
+		/*
+		 * Create list of Consts for the allowed values.
+		 */
+		foreach(cell, spec->listdatums)
+		{
+			List	   *andexpr = NIL;
+			ListCell   *cell2 = NULL;
+
+			j = 0;
+			foreach(cell2, (List *) lfirst(cell))
+			{
+				Const      *val = castNode(Const, lfirst(cell2));
+
+				if (val->constisnull)
+				{
+					nulltest = makeNode(NullTest);
+					nulltest->arg = keyCol[j];
+					nulltest->nulltesttype = IS_NULL;
+					nulltest->argisrow = false;
+					nulltest->location = -1;
+					andexpr = lappend(andexpr, nulltest);
+				}
+				else
+				{
+					opexpr = make_partition_op_expr(key, j, BTEqualStrategyNumber,
+													keyCol[j], (Expr *) val);
+					andexpr = lappend(andexpr, opexpr);
+				}
+				j++;
+			}
+
+			opexpr = makeBoolExpr(AND_EXPR, andexpr, -1);
+			elems = lappend(elems, opexpr);
+		}
+	}
+
+	opexpr = makeBoolExpr(OR_EXPR, elems, -1);
+	result = list_make1(opexpr);
+
+	/*
+	 * Note that, in general, applying NOT to a constraint expression doesn't
+	 * necessarily invert the set of rows it accepts, because NOT (NULL) is
+	 * NULL.  However, the partition constraints we construct here never
+	 * evaluate to NULL, so applying NOT works as intended.
+	 */
+	if (spec->is_default)
+		result = list_make1(makeBoolExpr(NOT_EXPR, result, -1));
+
+	return result;
+}
+
+/*
  * get_qual_for_range
  *
  * Returns an implicit-AND list of expressions to use as a range partition's
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index c793742..d4e9ba3 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -185,7 +185,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,
+												 StrategyNumber opstrategy,
+												 Datum *values, int nvalues,
 												 FmgrInfo *partsupfunc, Bitmapset *nullkeys);
 static PruneStepResult *get_matching_range_bounds(PartitionPruneContext *context,
 												  StrategyNumber opstrategy, Datum *values, int nvalues,
@@ -909,7 +910,8 @@ get_matching_partitions(PartitionPruneContext *context, List *pruning_steps)
 	{
 		Assert(context->strategy == PARTITION_STRATEGY_LIST);
 		Assert(partition_bound_accepts_nulls(context->boundinfo));
-		result = bms_add_member(result, context->boundinfo->null_index);
+		result = bms_add_member(result,
+								get_partition_bound_null_index(context->boundinfo));
 	}
 	if (scan_default)
 	{
@@ -2659,7 +2661,7 @@ get_matching_hash_bounds(PartitionPruneContext *context,
  */
 static PruneStepResult *
 get_matching_list_bounds(PartitionPruneContext *context,
-						 StrategyNumber opstrategy, Datum value, int nvalues,
+						 StrategyNumber opstrategy, Datum *values, int nvalues,
 						 FmgrInfo *partsupfunc, Bitmapset *nullkeys)
 {
 	PruneStepResult *result = (PruneStepResult *) palloc0(sizeof(PruneStepResult));
@@ -2672,7 +2674,6 @@ get_matching_list_bounds(PartitionPruneContext *context,
 	Oid		   *partcollation = context->partcollation;
 
 	Assert(context->strategy == PARTITION_STRATEGY_LIST);
-	Assert(context->partnatts == 1);
 
 	result->scan_null = result->scan_default = false;
 
@@ -2710,9 +2711,19 @@ get_matching_list_bounds(PartitionPruneContext *context,
 	 */
 	if (nvalues == 0)
 	{
+		int i = 0;
+		int j = 0;
+
 		Assert(boundinfo->ndatums > 0);
-		result->bound_offsets = bms_add_range(NULL, 0,
-											  boundinfo->ndatums - 1);
+
+		for (i = 0; i < boundinfo->ndatums; i++)
+		{
+			for (j = 0; j < context->partnatts; j++)
+			{
+				if (!boundinfo->isnulls[i][j])
+					result->bound_offsets = bms_add_member(result->bound_offsets, i);
+			}
+		}
 		result->scan_default = partition_bound_has_default(boundinfo);
 		return result;
 	}
@@ -2720,15 +2731,24 @@ get_matching_list_bounds(PartitionPruneContext *context,
 	/* Special case handling of values coming from a <> operator clause. */
 	if (opstrategy == InvalidStrategy)
 	{
+		int i = 0;
+		int j = 0;
+
 		/*
 		 * 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);
+		for (i = 0; i < boundinfo->ndatums; i++)
+		{
+			for (j = 0; j < context->partnatts; j++)
+			{
+				if (!boundinfo->isnulls[i][j])
+					result->bound_offsets = bms_add_member(result->bound_offsets, i);
+			}
+		}
 
 		off = partition_list_bsearch(partsupfunc, partcollation, boundinfo,
-									 value, &is_equal);
+									 nvalues, values, NULL, &is_equal);
 		if (off >= 0 && is_equal)
 		{
 
@@ -2760,8 +2780,8 @@ get_matching_list_bounds(PartitionPruneContext *context,
 		case BTEqualStrategyNumber:
 			off = partition_list_bsearch(partsupfunc,
 										 partcollation,
-										 boundinfo, value,
-										 &is_equal);
+										 boundinfo, nvalues,
+										 values, NULL, &is_equal);
 			if (off >= 0 && is_equal)
 			{
 				Assert(boundinfo->indexes[off] >= 0);
@@ -2777,8 +2797,9 @@ get_matching_list_bounds(PartitionPruneContext *context,
 		case BTGreaterStrategyNumber:
 			off = partition_list_bsearch(partsupfunc,
 										 partcollation,
-										 boundinfo, value,
-										 &is_equal);
+										 boundinfo, nvalues,
+										 values, NULL, &is_equal);
+
 			if (off >= 0)
 			{
 				/* We don't want the matched datum to be in the result. */
@@ -2812,8 +2833,9 @@ get_matching_list_bounds(PartitionPruneContext *context,
 		case BTLessStrategyNumber:
 			off = partition_list_bsearch(partsupfunc,
 										 partcollation,
-										 boundinfo, value,
-										 &is_equal);
+										 boundinfo, nvalues,
+										 values, NULL, &is_equal);
+
 			if (off >= 0 && is_equal && !inclusive)
 				off--;
 
@@ -3452,7 +3474,7 @@ perform_pruning_base_step(PartitionPruneContext *context,
 		case PARTITION_STRATEGY_LIST:
 			return get_matching_list_bounds(context,
 											opstep->opstrategy,
-											values[0], nvalues,
+											values, nvalues,
 											&partsupfunc[0],
 											opstep->nullkeys);
 
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 84ad62c..053aa6b 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9399,13 +9399,11 @@ get_rule_expr(Node *node, deparse_context *context,
 						sep = "";
 						foreach(cell, spec->listdatums)
 						{
-							Const	   *val = castNode(Const, lfirst(cell));
-
 							appendStringInfoString(buf, sep);
-							get_const_expr(val, context, -1);
+							appendStringInfoString
+								(buf, get_list_partbound_value_string(lfirst(cell)));
 							sep = ", ";
 						}
-
 						appendStringInfoChar(buf, ')');
 						break;
 
@@ -11963,6 +11961,45 @@ 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 = NULL;
+	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 ebf3ff1..a4b301b 100644
--- a/src/include/partitioning/partbounds.h
+++ b/src/include/partitioning/partbounds.h
@@ -67,20 +67,21 @@ typedef struct PartitionBoundInfoData
 	char		strategy;		/* hash, list or range? */
 	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 */
 	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_partition_bound_null_index(PartitionBoundInfo boundinfo);
+
 extern int	get_hash_partition_greatest_modulus(PartitionBoundInfo b);
 extern uint64 compute_partition_hash_value(int partnatts, FmgrInfo *partsupfunc,
 										   Oid *partcollation,
@@ -117,12 +118,17 @@ extern int32 partition_rbound_datum_cmp(FmgrInfo *partsupfunc,
 extern int	partition_list_bsearch(FmgrInfo *partsupfunc,
 								   Oid *partcollation,
 								   PartitionBoundInfo boundinfo,
-								   Datum value, bool *is_equal);
+								   int nvalues, Datum *values,
+								   bool *isnulls, bool *is_equal);
 extern int	partition_range_datum_bsearch(FmgrInfo *partsupfunc,
 										  Oid *partcollation,
 										  PartitionBoundInfo boundinfo,
 										  int nvalues, Datum *values, bool *is_equal);
 extern int	partition_hash_bsearch(PartitionBoundInfo boundinfo,
 								   int modulus, int remainder);
-
+extern int32 partition_lbound_datum_cmp(FmgrInfo *partsupfunc,
+										Oid *partcollation,
+										int nvalues, Datum *lb_datums,
+										bool *lb_isnulls, Datum *values,
+										bool *isnulls, bool *is_all_match);
 #endif							/* PARTBOUNDS_H */
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 ad89dd0..43f669d 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,
@@ -913,6 +907,34 @@ 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;
+-- 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..174f8c2 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;
+   tableoid   | a | b | c 
+--------------+---+---+---
+ mclparted_p1 | 1 | a | 1
+ mclparted_p2 | 1 | b | 1
+ mclparted_p2 | 2 | a | 1
+ mclparted_p2 | 1 | a | 2
+ mclparted_p3 | 3 | c | 3
+ mclparted_p3 | 5 | e | 5
+ mclparted_p3 | 6 |   | 6
+ mclparted_p3 | 4 | d | 4
+ mclparted_p4 |   | a | 1
+ mclparted_p4 | 1 |   | 1
+ mclparted_p4 | 1 | a |  
+ 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 = '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 = '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/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index 54cbf6c..8462ff0 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,
@@ -725,6 +719,25 @@ 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;
+
+-- 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..76e0d00 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;
+
+-- 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);
#9Amit Langote
amitlangote09@gmail.com
In reply to: Nitin Jadhav (#8)
1 attachment(s)
Re: Multi-Column List Partitioning

Hi Nitin,

On Thu, Jun 3, 2021 at 11:45 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

I'll wait for you to post a new patch addressing at least the comments
in my earlier email. Also, please make sure to run `make check`
successfully before posting the patch. :)

I have fixed all of the review comments given by you and Jeevan in the
attached patch and also the attached patch contains more changes
compared to the previous patch. Following are the implementation
details.

Thanks for the updated version.

1. Regarding syntax, the existing syntax will work fine for the
single-column list partitioning. However I have used the new syntax
for the multi-column list partitioning as we discussed earlier. I have
used a combination of 'AND' and 'OR' logic for the partition
constraints as given in the below example.

postgres@17503=#create table t(a int, b text) partition by list(a,b);
CREATE TABLE
postgres@17503=#create table t1 partition of t for values in ((1,'a'),
(NULL,'b'));
CREATE TABLE
postgres@17503=#\d+ t
Partitioned table "public.t"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+----------+-------------+--------------+-------------
a | integer | | | | plain |
| |
b | text | | | | extended |
| |
Partition key: LIST (a, b)
Partitions: t1 FOR VALUES IN ((1, 'a'), (NULL, 'b'))

postgres@17503=#\d+ t1
Table "public.t1"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+----------+-------------+--------------+-------------
a | integer | | | | plain |
| |
b | text | | | | extended |
| |
Partition of: t FOR VALUES IN ((1, 'a'), (NULL, 'b'))
Partition constraint: (((a = 1) AND (b = 'a'::text)) OR ((a IS NULL)
AND (b = 'b'::text)))
Access method: heap

The constraint expressions seem to come out correctly, though I
haven't checked your implementation closely yet.

2. In the existing code, NULL values were handled differently. It was
not added to the 'datums' variable, rather used to store the partition
index directly in the 'null_index' variable. Now there is a
possibility of multiple NULL values, hence introducing a new member
'isnulls' in the 'PartitionBoundInfoData' struct which indicates
whether the corresponding element in the 'datums' is NULL. Now
'null_index' cannot be used directly to store the partition index, so
removed it and made the necessary changes in multiple places.

3. I have added test cases for 'create table' and 'insert' statements
related to multi-column list partitioning and these are working fine
with 'make check'.

4. Handled the partition pruning code to accommodate these changes for
single-column list partitioning. However it is pending for
multi-column list partitioning.

5. I have done necessary changes in partition wise join related code
to accommodate for single-column list partitioning. However it is
pending for multi-column list partitioning.

Kindly review the patch and let me know if any changes are required.

The new list bound binary search and related comparison support
function look a bit too verbose to me. I was expecting
partition_list_bsearch() to look very much like
partition_range_datum_bsearch(), but that is not the case. The
special case code that you wrote in partition_list_bsearch() seems
unnecessary, at least in that function. I'm talking about the code
fragment starting with this comment:

/*
* Once we find the matching for the first column but if it does not
* match for the any of the other columns, then the binary search
* will not work in all the cases. We should traverse just below
* and above the mid index until we find the match or we reach the
* first mismatch.
*/

I guess you're perhaps trying to address the case where the caller
does not specify the values for all of the partition key columns,
which can happen when the partition pruning code needs to handle a set
of clauses matching only some of the partition key columns. But
that's a concern of the partition pruning code and so the special case
should be handled there (if at all), not in the binary search function
that is shared with other callers. Regarding that, I'm wondering if
we should require clauses matching all of the partition key columns to
be found for the pruning code to call the binary search, so do
something like get_matching_hash_bounds() does:

/*
* For hash partitioning we can only perform pruning based on equality
* clauses to the partition key or IS NULL clauses. We also can only
* prune if we got values for all keys.
*/
if (nvalues + bms_num_members(nullkeys) == partnatts)
{
/* code to compute matching hash bound offset */
}
else
{
/* Report all valid offsets into the boundinfo->indexes array. */
result->bound_offsets = bms_add_range(NULL, 0,
boundinfo->nindexes - 1);
}

Do you think that trying to match list partitions even with fewer keys
is worth the complexity of the implementation? That is, is the use
case to search for only a subset of partition key columns common
enough with list partitioning?

If we do decide to implement the special case, remember that to do
that efficiently, we'd need to require that the subset of matched key
columns constitutes a prefix, because of the way the datums are
sorted. That is, match all partitions when the query only contains a
clause for b when the partition key is (a, b, c), but engage the
special case of pruning if the query contains clauses for a, or for a
and b.

I will look at other parts of the patch next week hopefully. For
now, attached is a delta patch that applies on top of your v1, which
does:

* Simplify partition_list_bsearch() and partition_lbound_datum_cmp()
* Make qsort_partition_list_value_cmp simply call
partition_lbound_datum_cmp() instead of having its own logic to
compare input bounds
* Move partition_lbound_datum_cmp() into partbounds.c as a static
function (export seems unnecessary)
* Add a comment for PartitionBoundInfo.isnulls and remove that for null_index

--
Amit Langote
EDB: http://www.enterprisedb.com

Attachments:

v1_delta_amit.patchapplication/octet-stream; name=v1_delta_amit.patchDownload
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index bb6cddc35e..a38c87bcc1 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -103,7 +103,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,
@@ -222,6 +223,9 @@ static int32 partition_rbound_cmp(int partnatts, FmgrInfo *partsupfunc,
 								  Oid *partcollation, Datum *datums1,
 								  PartitionRangeDatumKind *kind1, bool lower1,
 								  PartitionRangeBound *b2);
+static int32 partition_lbound_datum_cmp(FmgrInfo *partsupfunc, Oid *partcollation,
+						   int nvalues, Datum *lb_datums, bool *lb_isnulls,
+						   Datum *values, bool *isnulls);
 static int	partition_range_bsearch(int partnatts, FmgrInfo *partsupfunc,
 									Oid *partcollation,
 									PartitionBoundInfo boundinfo,
@@ -1111,7 +1115,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,
@@ -1155,7 +1160,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)
@@ -1221,7 +1227,6 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		bool	   *outer_isnull;
 		bool	   *inner_isnull;
 		bool	   *merged_isnull = NULL;
-		bool 		is_all_match = false;
 
 		if (outer_bi->isnulls && outer_pos < outer_bi->ndatums)
 			outer_isnull = outer_bi->isnulls[outer_pos];
@@ -1298,14 +1303,15 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		{
 			Assert(outer_datums != NULL && inner_datums != NULL);
 			//TODO: handle multi-column case
-			cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation, 1,  //TODO: get attr count
+			cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+												partnatts,
 												outer_datums,
 												outer_isnull,
 												inner_datums,
-												inner_isnull, &is_all_match);
+												inner_isnull);
 		}
 
-		if (is_all_match)
+		if (cmpval == 0)
 		{
 			/* Two list values match exactly. */
 			Assert(outer_pos < outer_bi->ndatums);
@@ -3634,32 +3640,33 @@ partition_hbound_cmp(int modulus1, int remainder1, int modulus2, int remainder2)
 /*
  * partition_lbound_datum_cmp
  *
- * This function compares the list bound values of all the partition key
- * columns. Returns the value of 'cmpval' if the first bound value does
- * not match, otherwise returns zero. If it successfully compares the bound
- * values for all the partition key, then it sets is_all_match to TRUE.
+ * 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
+static int32
 partition_lbound_datum_cmp(FmgrInfo *partsupfunc, Oid *partcollation,
 						   int nvalues, Datum *lb_datums, bool *lb_isnulls,
-						   Datum *values, bool *isnulls, bool *is_all_match)
+						   Datum *values, bool *isnulls)
 {
-	int		i = 0;
-	int32	cmpval = 0;
+	int		i;
+	int32	cmpval;
 
 	for (i = 0; i < nvalues; i++)
 	{
-		bool isnull = false;
-
-		if (isnulls)
-			isnull = isnulls[i];
-
-		if (lb_isnulls[i] && isnull)
-			cmpval = 0;
-		else if (lb_isnulls[i])
-			cmpval = 1;
-		else if (isnull)
-			cmpval = -1;
+		/* 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],
@@ -3669,13 +3676,7 @@ partition_lbound_datum_cmp(FmgrInfo *partsupfunc, Oid *partcollation,
 			break;
 	}
 
-	if (i == 0)
-		return cmpval;
-
-	if (i == nvalues && cmpval == 0)
-		*is_all_match = true;
-
-	return 0;
+	return cmpval;
 }
 
 /*
@@ -3700,76 +3701,17 @@ partition_list_bsearch(FmgrInfo *partsupfunc, Oid *partcollation,
 	while (lo < hi)
 	{
 		int32		cmpval;
-		bool		is_all_match = false;
 
 		mid = (lo + hi + 1) / 2;
 		cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
-											nvalues, boundinfo->datums[mid],
-											boundinfo->isnulls[mid], values,
-											isnulls, &is_all_match);
-
-		if (is_all_match)
-		{
-			*is_equal = true;
-			return mid;
-		}
-
-		if (cmpval == 0)
-		{
-			/*
-			 * Once we find the matching for the first column but if it does not
-			 * match for the any of the other columns, then the binary search
-			 * will not work in all the cases. We should traverse just below
-			 * and above the mid index until we find the match or we reach the
-			 * first mismatch.
-			 */
-			int32	off = mid;
-			while (off >= 1)
-			{
-				cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
-													nvalues, boundinfo->datums[off - 1],
-													boundinfo->isnulls[off - 1], values,
-													isnulls, &is_all_match);
-
-				if (is_all_match)
-				{
-					/* Found the matching bound. Return the offset. */
-					*is_equal = true;
-					return (off - 1);
-				}
-
-				if (cmpval != 0)
-					break;
-
-				off--;
-			}
-
-			off = mid;
-			while (off < boundinfo->ndatums - 1)
-			{
-				cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
-													nvalues, boundinfo->datums[off + 1],
-													boundinfo->isnulls[off + 1], values,
-													isnulls, &is_all_match);
-
-				if (is_all_match)
-				{
-					/* Found the matching bound. Return the offset. */
-					*is_equal = true;
-					return (off + 1);
-				}
-
-				if (cmpval != 0)
-					break;
-
-				off++;
-			}
-		}
-
+											nvalues,
+											boundinfo->datums[mid],
+											boundinfo->isnulls[mid],
+											values, isnulls);
 		if (cmpval <= 0)
 		{
 			lo = mid;
-			*is_equal = (cmpval == 0 && is_all_match);
+			*is_equal = (cmpval == 0);
 			if (*is_equal)
 				break;
 		}
@@ -3935,23 +3877,17 @@ 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)->values;
-	Datum	   *val2 = (*(PartitionListValue *const *) b)->values;
-	bool	   *null1 = (*(PartitionListValue *const *) a)->isnulls;
-	bool	   *null2 = (*(PartitionListValue *const *) b)->isnulls;
-
+	Datum	   *vals1 = (*(PartitionListValue *const *) a)->values;
+	Datum	   *vals2 = (*(PartitionListValue *const *) b)->values;
+	bool	   *isnull1 = (*(PartitionListValue *const *) a)->isnulls;
+	bool	   *isnull2 = (*(PartitionListValue *const *) b)->isnulls;
 	PartitionKey key = (PartitionKey) arg;
 
-	if (null1[0] && null2[0])
-		return 0;
-	else if (null1[0])
-		return 1;
-	else if (null2[0])
-		return -1;
-	else
-		return DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[0],
-											   key->partcollation[0],
-											   val1[0], val2[0]));
+	return partition_lbound_datum_cmp(key->partsupfunc,
+									  key->partcollation,
+									  key->partnatts,
+									  vals1, isnull1,
+									  vals2, isnull2);
 }
 
 /*
diff --git a/src/include/partitioning/partbounds.h b/src/include/partitioning/partbounds.h
index a4b301bfa1..f46ad3ad23 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 booleans 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
@@ -126,9 +127,4 @@ extern int	partition_range_datum_bsearch(FmgrInfo *partsupfunc,
 										  int nvalues, Datum *values, bool *is_equal);
 extern int	partition_hash_bsearch(PartitionBoundInfo boundinfo,
 								   int modulus, int remainder);
-extern int32 partition_lbound_datum_cmp(FmgrInfo *partsupfunc,
-										Oid *partcollation,
-										int nvalues, Datum *lb_datums,
-										bool *lb_isnulls, Datum *values,
-										bool *isnulls, bool *is_all_match);
 #endif							/* PARTBOUNDS_H */
diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out
index 174f8c2d19..8e704d3928 100644
--- a/src/test/regress/expected/insert.out
+++ b/src/test/regress/expected/insert.out
@@ -846,20 +846,20 @@ 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;
+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_p2 | 1 | a | 2
  mclparted_p3 | 3 | c | 3
+ mclparted_p3 | 4 | d | 4
  mclparted_p3 | 5 | e | 5
  mclparted_p3 | 6 |   | 6
- mclparted_p3 | 4 | d | 4
- mclparted_p4 |   | a | 1
- mclparted_p4 | 1 |   | 1
  mclparted_p4 | 1 | a |  
+ mclparted_p4 | 1 |   | 1
+ mclparted_p4 |   | a | 1
  mclparted_p5 |   |   |  
 (12 rows)
 
diff --git a/src/test/regress/sql/insert.sql b/src/test/regress/sql/insert.sql
index 76e0d004a1..2bfc55c66a 100644
--- a/src/test/regress/sql/insert.sql
+++ b/src/test/regress/sql/insert.sql
@@ -573,7 +573,7 @@ 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;
+select tableoid::regclass::text, * from mclparted order by 1, 2, 3, 4;
 
 -- cleanup
 drop table mclparted;
#10Amit Langote
amitlangote09@gmail.com
In reply to: Amit Langote (#9)
Re: Multi-Column List Partitioning

On Fri, Jun 11, 2021 at 12:37 PM Amit Langote <amitlangote09@gmail.com> wrote:

I will look at other parts of the patch next week hopefully. For
now, attached is a delta patch that applies on top of your v1, which
does:

* Simplify partition_list_bsearch() and partition_lbound_datum_cmp()
* Make qsort_partition_list_value_cmp simply call
partition_lbound_datum_cmp() instead of having its own logic to
compare input bounds
* Move partition_lbound_datum_cmp() into partbounds.c as a static
function (export seems unnecessary)
* Add a comment for PartitionBoundInfo.isnulls and remove that for null_index

One more:

* Add all columns of newly added test query in insert.sql to the order
by clause to get predictably ordered output

--
Amit Langote
EDB: http://www.enterprisedb.com

#11Zhihong Yu
zyu@yugabyte.com
In reply to: Amit Langote (#9)
Re: Multi-Column List Partitioning

On Thu, Jun 10, 2021 at 8:38 PM Amit Langote <amitlangote09@gmail.com>
wrote:

Hi Nitin,

On Thu, Jun 3, 2021 at 11:45 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

I'll wait for you to post a new patch addressing at least the comments
in my earlier email. Also, please make sure to run `make check`
successfully before posting the patch. :)

I have fixed all of the review comments given by you and Jeevan in the
attached patch and also the attached patch contains more changes
compared to the previous patch. Following are the implementation
details.

Thanks for the updated version.

1. Regarding syntax, the existing syntax will work fine for the
single-column list partitioning. However I have used the new syntax
for the multi-column list partitioning as we discussed earlier. I have
used a combination of 'AND' and 'OR' logic for the partition
constraints as given in the below example.

postgres@17503=#create table t(a int, b text) partition by list(a,b);
CREATE TABLE
postgres@17503=#create table t1 partition of t for values in ((1,'a'),
(NULL,'b'));
CREATE TABLE
postgres@17503=#\d+ t
Partitioned table "public.t"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description

--------+---------+-----------+----------+---------+----------+-------------+--------------+-------------

a | integer | | | | plain |
| |
b | text | | | | extended |
| |
Partition key: LIST (a, b)
Partitions: t1 FOR VALUES IN ((1, 'a'), (NULL, 'b'))

postgres@17503=#\d+ t1
Table "public.t1"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description

--------+---------+-----------+----------+---------+----------+-------------+--------------+-------------

a | integer | | | | plain |
| |
b | text | | | | extended |
| |
Partition of: t FOR VALUES IN ((1, 'a'), (NULL, 'b'))
Partition constraint: (((a = 1) AND (b = 'a'::text)) OR ((a IS NULL)
AND (b = 'b'::text)))
Access method: heap

The constraint expressions seem to come out correctly, though I
haven't checked your implementation closely yet.

2. In the existing code, NULL values were handled differently. It was
not added to the 'datums' variable, rather used to store the partition
index directly in the 'null_index' variable. Now there is a
possibility of multiple NULL values, hence introducing a new member
'isnulls' in the 'PartitionBoundInfoData' struct which indicates
whether the corresponding element in the 'datums' is NULL. Now
'null_index' cannot be used directly to store the partition index, so
removed it and made the necessary changes in multiple places.

3. I have added test cases for 'create table' and 'insert' statements
related to multi-column list partitioning and these are working fine
with 'make check'.

4. Handled the partition pruning code to accommodate these changes for
single-column list partitioning. However it is pending for
multi-column list partitioning.

5. I have done necessary changes in partition wise join related code
to accommodate for single-column list partitioning. However it is
pending for multi-column list partitioning.

Kindly review the patch and let me know if any changes are required.

The new list bound binary search and related comparison support
function look a bit too verbose to me. I was expecting
partition_list_bsearch() to look very much like
partition_range_datum_bsearch(), but that is not the case. The
special case code that you wrote in partition_list_bsearch() seems
unnecessary, at least in that function. I'm talking about the code
fragment starting with this comment:

/*
* Once we find the matching for the first column but if it does
not
* match for the any of the other columns, then the binary search
* will not work in all the cases. We should traverse just below
* and above the mid index until we find the match or we reach
the
* first mismatch.
*/

I guess you're perhaps trying to address the case where the caller
does not specify the values for all of the partition key columns,
which can happen when the partition pruning code needs to handle a set
of clauses matching only some of the partition key columns. But
that's a concern of the partition pruning code and so the special case
should be handled there (if at all), not in the binary search function
that is shared with other callers. Regarding that, I'm wondering if
we should require clauses matching all of the partition key columns to
be found for the pruning code to call the binary search, so do
something like get_matching_hash_bounds() does:

/*
* For hash partitioning we can only perform pruning based on equality
* clauses to the partition key or IS NULL clauses. We also can only
* prune if we got values for all keys.
*/
if (nvalues + bms_num_members(nullkeys) == partnatts)
{
/* code to compute matching hash bound offset */
}
else
{
/* Report all valid offsets into the boundinfo->indexes array. */
result->bound_offsets = bms_add_range(NULL, 0,
boundinfo->nindexes - 1);
}

Do you think that trying to match list partitions even with fewer keys
is worth the complexity of the implementation? That is, is the use
case to search for only a subset of partition key columns common
enough with list partitioning?

If we do decide to implement the special case, remember that to do
that efficiently, we'd need to require that the subset of matched key
columns constitutes a prefix, because of the way the datums are
sorted. That is, match all partitions when the query only contains a
clause for b when the partition key is (a, b, c), but engage the
special case of pruning if the query contains clauses for a, or for a
and b.

I will look at other parts of the patch next week hopefully. For
now, attached is a delta patch that applies on top of your v1, which
does:

* Simplify partition_list_bsearch() and partition_lbound_datum_cmp()
* Make qsort_partition_list_value_cmp simply call
partition_lbound_datum_cmp() instead of having its own logic to
compare input bounds
* Move partition_lbound_datum_cmp() into partbounds.c as a static
function (export seems unnecessary)
* Add a comment for PartitionBoundInfo.isnulls and remove that for
null_index

--
Amit Langote
EDB: http://www.enterprisedb.com

Hi, Amit:

+ * isnulls is an array of boolean-tuples with key->partnatts booleans
values
+ * each.  Currently only used for list partitioning, it stores whether a

I think 'booleans' should be 'boolean'.
The trailing word 'each' is unnecessary.

Cheers

#12Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Zhihong Yu (#11)
1 attachment(s)
Re: Multi-Column List Partitioning

The new list bound binary search and related comparison support
function look a bit too verbose to me. I was expecting
partition_list_bsearch() to look very much like
partition_range_datum_bsearch(), but that is not the case. The
special case code that you wrote in partition_list_bsearch() seems
unnecessary, at least in that function. I'm talking about the code
fragment starting with this comment:

I will look at other parts of the patch next week hopefully. For
now, attached is a delta patch that applies on top of your v1, which
does:

* Simplify partition_list_bsearch() and partition_lbound_datum_cmp()
* Make qsort_partition_list_value_cmp simply call
partition_lbound_datum_cmp() instead of having its own logic to
compare input bounds
* Move partition_lbound_datum_cmp() into partbounds.c as a static
function (export seems unnecessary)
* Add a comment for PartitionBoundInfo.isnulls and remove that for null_index

Yes. You are right. The extra code added in partition_list_bsearch()
is not required and thanks for sharing the delta patch. It looks good
to me and I have incorporated the changes in the attached patch.

I guess you're perhaps trying to address the case where the caller
does not specify the values for all of the partition key columns,
which can happen when the partition pruning code needs to handle a set
of clauses matching only some of the partition key columns. But
that's a concern of the partition pruning code and so the special case
should be handled there (if at all), not in the binary search function
that is shared with other callers. Regarding that, I'm wondering if
we should require clauses matching all of the partition key columns to
be found for the pruning code to call the binary search, so do
something like get_matching_hash_bounds() does:

Do you think that trying to match list partitions even with fewer keys
is worth the complexity of the implementation? That is, is the use
case to search for only a subset of partition key columns common
enough with list partitioning?

If we do decide to implement the special case, remember that to do
that efficiently, we'd need to require that the subset of matched key
columns constitutes a prefix, because of the way the datums are
sorted. That is, match all partitions when the query only contains a
clause for b when the partition key is (a, b, c), but engage the
special case of pruning if the query contains clauses for a, or for a
and b.

Thanks for the suggestion. Below is the implementation details for the
partition pruning for multi column list partitioning.

In the existing code (For single column list partitioning)
1. In gen_partprune_steps_internal(), we try to match the where
clauses provided by the user with the partition key data using
match_clause_to_partition_key(). Based on the match, this function can
return many values like PARTCLAUSE_MATCH_CLAUSE,
PARTCLAUSE_MATCH_NULLNESS, PARTCLAUSE_NOMATCH, etc.
2. In case of PARTCLAUSE_MATCH_CLAUSE, we generate steps using
gen_prune_steps_from_opexps() (strategy-2) which generate and return a
list of PartitionPruneStepOp that are based on OpExpr and BooleanTest
clauses that have been matched to the partition key and it also takes
care handling prefix of the partition keys.
3. In case of PARTCLAUSE_MATCH_NULLNESS, we generate steps using
gen_prune_step_op() (strategy-1) which generates single
PartitionPruneStepOp since the earlier list partitioning supports
single column and there can be only one NULL value. In
get_matching_list_bounds(), if the nullkeys is not empty, we fetch the
partition index which accepts null and we used to return from here.

In case of multi column list partitioning, we have columns more than
one and hence there is a possibility of more than one NULL values in
the where clauses. The above mentioned steps are modified like below.

1. Modified the match_clause_to_partition_key() to generate an object
of PartClauseInfo structure and return PARTCLAUSE_MATCH_CLAUSE even in
case of clauses related to NULL. The information required to generate
PartClauseInfo is populated here like the constant expression
consisting of (Datum) 0, op_strategy, op_is_ne, etc.
2. Since I am returning PARTCLAUSE_MATCH_CLAUSE, now we use strategy-2
(gen_prune_steps_from_opexps) to generate partition pruning steps.
This function takes care of generating a list of pruning steps if
there are multiple clauses and also takes care of handling prefixes.
3. Modified perform_pruning_base_step() to generate the datum values
and isnulls data of the where clauses. In case if any of the key
contains NULL value then the corresponding datum value is 0.
4. Modified get_matching_list_bounds() to generate the minimum offset
and/or maximum offset of the matched values based on the difference
operation strategies. Now since the NULL containing bound values are
part of 'boundinfo', changed the code accordingly to include the NULL
containing partitions or not in different scenarios like
InvalidStrategy, etc.

I have done some cosmetic changes to
v1_multi_column_list_partitioning.patch. So all the above code changes
related to partition pruning are merged with the previous patch and
also included the delta patch shared by you. Hence sharing a single
patch.

Kindly have a look and share your thoughts.

Show quoted text

On Fri, Jun 11, 2021 at 10:57 PM Zhihong Yu <zyu@yugabyte.com> wrote:

On Thu, Jun 10, 2021 at 8:38 PM Amit Langote <amitlangote09@gmail.com> wrote:

Hi Nitin,

On Thu, Jun 3, 2021 at 11:45 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

I'll wait for you to post a new patch addressing at least the comments
in my earlier email. Also, please make sure to run `make check`
successfully before posting the patch. :)

I have fixed all of the review comments given by you and Jeevan in the
attached patch and also the attached patch contains more changes
compared to the previous patch. Following are the implementation
details.

Thanks for the updated version.

1. Regarding syntax, the existing syntax will work fine for the
single-column list partitioning. However I have used the new syntax
for the multi-column list partitioning as we discussed earlier. I have
used a combination of 'AND' and 'OR' logic for the partition
constraints as given in the below example.

postgres@17503=#create table t(a int, b text) partition by list(a,b);
CREATE TABLE
postgres@17503=#create table t1 partition of t for values in ((1,'a'),
(NULL,'b'));
CREATE TABLE
postgres@17503=#\d+ t
Partitioned table "public.t"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+----------+-------------+--------------+-------------
a | integer | | | | plain |
| |
b | text | | | | extended |
| |
Partition key: LIST (a, b)
Partitions: t1 FOR VALUES IN ((1, 'a'), (NULL, 'b'))

postgres@17503=#\d+ t1
Table "public.t1"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+----------+-------------+--------------+-------------
a | integer | | | | plain |
| |
b | text | | | | extended |
| |
Partition of: t FOR VALUES IN ((1, 'a'), (NULL, 'b'))
Partition constraint: (((a = 1) AND (b = 'a'::text)) OR ((a IS NULL)
AND (b = 'b'::text)))
Access method: heap

The constraint expressions seem to come out correctly, though I
haven't checked your implementation closely yet.

2. In the existing code, NULL values were handled differently. It was
not added to the 'datums' variable, rather used to store the partition
index directly in the 'null_index' variable. Now there is a
possibility of multiple NULL values, hence introducing a new member
'isnulls' in the 'PartitionBoundInfoData' struct which indicates
whether the corresponding element in the 'datums' is NULL. Now
'null_index' cannot be used directly to store the partition index, so
removed it and made the necessary changes in multiple places.

3. I have added test cases for 'create table' and 'insert' statements
related to multi-column list partitioning and these are working fine
with 'make check'.

4. Handled the partition pruning code to accommodate these changes for
single-column list partitioning. However it is pending for
multi-column list partitioning.

5. I have done necessary changes in partition wise join related code
to accommodate for single-column list partitioning. However it is
pending for multi-column list partitioning.

Kindly review the patch and let me know if any changes are required.

The new list bound binary search and related comparison support
function look a bit too verbose to me. I was expecting
partition_list_bsearch() to look very much like
partition_range_datum_bsearch(), but that is not the case. The
special case code that you wrote in partition_list_bsearch() seems
unnecessary, at least in that function. I'm talking about the code
fragment starting with this comment:

/*
* Once we find the matching for the first column but if it does not
* match for the any of the other columns, then the binary search
* will not work in all the cases. We should traverse just below
* and above the mid index until we find the match or we reach the
* first mismatch.
*/

I guess you're perhaps trying to address the case where the caller
does not specify the values for all of the partition key columns,
which can happen when the partition pruning code needs to handle a set
of clauses matching only some of the partition key columns. But
that's a concern of the partition pruning code and so the special case
should be handled there (if at all), not in the binary search function
that is shared with other callers. Regarding that, I'm wondering if
we should require clauses matching all of the partition key columns to
be found for the pruning code to call the binary search, so do
something like get_matching_hash_bounds() does:

/*
* For hash partitioning we can only perform pruning based on equality
* clauses to the partition key or IS NULL clauses. We also can only
* prune if we got values for all keys.
*/
if (nvalues + bms_num_members(nullkeys) == partnatts)
{
/* code to compute matching hash bound offset */
}
else
{
/* Report all valid offsets into the boundinfo->indexes array. */
result->bound_offsets = bms_add_range(NULL, 0,
boundinfo->nindexes - 1);
}

Do you think that trying to match list partitions even with fewer keys
is worth the complexity of the implementation? That is, is the use
case to search for only a subset of partition key columns common
enough with list partitioning?

If we do decide to implement the special case, remember that to do
that efficiently, we'd need to require that the subset of matched key
columns constitutes a prefix, because of the way the datums are
sorted. That is, match all partitions when the query only contains a
clause for b when the partition key is (a, b, c), but engage the
special case of pruning if the query contains clauses for a, or for a
and b.

I will look at other parts of the patch next week hopefully. For
now, attached is a delta patch that applies on top of your v1, which
does:

* Simplify partition_list_bsearch() and partition_lbound_datum_cmp()
* Make qsort_partition_list_value_cmp simply call
partition_lbound_datum_cmp() instead of having its own logic to
compare input bounds
* Move partition_lbound_datum_cmp() into partbounds.c as a static
function (export seems unnecessary)
* Add a comment for PartitionBoundInfo.isnulls and remove that for null_index

--
Amit Langote
EDB: http://www.enterprisedb.com

Hi, Amit:

+ * isnulls is an array of boolean-tuples with key->partnatts booleans values
+ * each.  Currently only used for list partitioning, it stores whether a

I think 'booleans' should be 'boolean'.
The trailing word 'each' is unnecessary.

Cheers

Attachments:

v2-0001-multi-column-list-partitioning.patchapplication/octet-stream; name=v2-0001-multi-column-list-partitioning.patchDownload
From 9a64d9b1bcb0d9cda45ec716f579d2cc44122f92 Mon Sep 17 00:00:00 2001
From: Nitin <nitin.jadhav@enterprisedb.com>
Date: Wed, 25 Aug 2021 17:57:27 +0530
Subject: [PATCH] Multi Column List Partitioning

Supported list partitioning based on multiple columns.
Supported new syantx to allow mentioning multiple key information.
Created a infrastructure to accomodate multiple NULL values in
case of list partitioning. Supported partition pruning mechanism
to work for multiple keys.
---
 src/backend/commands/tablecmds.c              |   7 -
 src/backend/executor/execPartition.c          |  10 +-
 src/backend/parser/parse_utilcmd.c            | 198 +++++---
 src/backend/partitioning/partbounds.c         | 625 ++++++++++++++++++++------
 src/backend/partitioning/partprune.c          | 439 +++++++++++++-----
 src/backend/utils/adt/ruleutils.c             |  45 +-
 src/include/partitioning/partbounds.h         |  22 +-
 src/include/utils/ruleutils.h                 |   1 +
 src/test/regress/expected/create_table.out    |  34 +-
 src/test/regress/expected/insert.out          | 147 ++++++
 src/test/regress/expected/partition_prune.out | 432 ++++++++++++++++++
 src/test/regress/sql/create_table.sql         |  25 +-
 src/test/regress/sql/insert.sql               |  64 +++
 src/test/regress/sql/partition_prune.sql      |  42 ++
 14 files changed, 1752 insertions(+), 339 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a030771..ac0812c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16669,13 +16669,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 675e400..756f3ec 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)
 }
 
 /*
+ * checkForDuplicates
+ *
+ * Returns TRUE if the list bound element is already present in the list of
+ * list bounds, FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)
+{
+	ListCell   *cell = NULL;
+
+	foreach(cell, source)
+	{
+		int		i = 0;
+		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(searchElem, 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,112 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 }
 
 /*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw grammar
+ * representation.
+ */
+static List *
+transformPartitionListBounds(ParseState *pstate, PartitionBoundSpec *spec,
+							 Relation parent)
+{
+	int				i = 0;
+	int				j = 0;
+	ListCell	   *cell = NULL;
+	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] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+			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;
+		bool		isDuplicate = false;
+
+		if (partnatts == 1)
+		{
+			Const	   *val =
+				transformPartitionBoundValue(pstate, expr,colname[0],
+											 coltype[0], coltypmod[0],
+											 partcollation[0]);
+			values = lappend(values, val);
+		}
+		else
+		{
+			ListCell   *cell2 = NULL;
+			RowExpr		*rowexpr = NULL;
+
+			if (!IsA(expr, RowExpr))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("Invalid list bound specification"),
+						parser_errposition(pstate, exprLocation((Node *) spec))));
+
+			rowexpr = (RowExpr *) expr;
+			if (partnatts != list_length(rowexpr->args))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("Must specify exactly one value per partitioning column"),
+						 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 */
+		isDuplicate = checkForDuplicates(result, values);
+		if (isDuplicate)
+			continue;
+
+		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 fdfe712..ca1e16b 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -53,11 +53,12 @@ typedef struct PartitionHashBound
 	int			index;
 } PartitionHashBound;
 
-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
 typedef struct PartitionListValue
 {
 	int			index;
-	Datum		value;
+	Datum	   *values;
+	bool	   *isnulls;
 } PartitionListValue;
 
 /* One bound of a range partition */
@@ -102,7 +103,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,
@@ -175,6 +177,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,
@@ -230,6 +233,7 @@ static Oid	get_partition_operator(PartitionKey key, int col,
 								   StrategyNumber strategy, bool *need_relabel);
 static List *get_qual_for_hash(Relation parent, PartitionBoundSpec *spec);
 static List *get_qual_for_list(Relation parent, PartitionBoundSpec *spec);
+static List *get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec);
 static List *get_qual_for_range(Relation parent, PartitionBoundSpec *spec,
 								bool for_default);
 static void get_range_key_properties(PartitionKey key, int keynum,
@@ -366,7 +370,7 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
 	/* No special hash partitions. */
-	boundinfo->null_index = -1;
+	boundinfo->isnulls = NULL;
 	boundinfo->default_index = -1;
 
 	hbounds = (PartitionHashBound *)
@@ -438,28 +442,72 @@ 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 partition bound has 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 = 0;
+	int j = 0;
 
-	for (i = 0; i < nparts; i++)
-	{
-		ListCell   *lc;
+	if (!boundinfo->isnulls)
+		return false;
 
-		foreach(lc, boundspecs[i]->listdatums)
+	for (i = 0; i < boundinfo->ndatums; i++)
+	{
+		//TODO: Handle for multi-column cases
+		for (j = 0; j < 1; j++)
 		{
-			Const	   *val = lfirst_node(Const, lc);
+			if (boundinfo->isnulls[i][j])
+				return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * get_partition_bound_null_index
+ *
+ * Returns the partition index of the partition bound which accepts NULL.
+ */
+int
+get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+{
+	int i = 0;
+	int j = 0;
+
+	if (!boundinfo->isnulls)
+		return -1;
 
-			if (!val->constisnull)
-				count++;
+	for (i = 0; i < boundinfo->ndatums; i++)
+	{
+		//TODO: Handle for multi-column cases
+		for (j = 0; j < 1; j++)
+		{
+			if (boundinfo->isnulls[i][j])
+				return boundinfo->indexes[i];
 		}
 	}
 
+	return -1;
+}
+
+/*
+ * get_list_datum_count
+ * 		Counts the number of Datums in each partition.
+ */
+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 +520,23 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 				   PartitionKey key, int **mapping)
 {
 	PartitionBoundInfo boundinfo;
-	PartitionListValue *all_values;
+	PartitionListValue **all_values;
 	int			i;
 	int			j;
 	int			ndatums;
 	int			next_index = 0;
 	int			default_index = -1;
-	int			null_index = -1;
 	Datum	   *boundDatums;
 
 	boundinfo = (PartitionBoundInfoData *)
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
 	/* 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 = (PartitionListValue **)
+		palloc(ndatums * sizeof(PartitionListValue *));
 
 	/* Create a unified list of non-null values across all partitions. */
 	for (j = 0, i = 0; i < nparts; i++)
@@ -514,35 +560,40 @@ 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 = NULL;
 
-			if (!val->constisnull)
-			{
-				all_values[j].index = i;
-				all_values[j].value = val->constvalue;
-				j++;
-			}
-			else
+			all_values[j] = (PartitionListValue *) palloc(sizeof(PartitionListValue));
+			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(PartitionListValue *),
 			  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;
@@ -563,12 +614,21 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	 */
 	for (i = 0; i < ndatums; i++)
 	{
-		int			orig_index = all_values[i].index;
+		int         j = 0;
+		int			orig_index = all_values[i]->index;
+		boundinfo->datums[i] = (Datum *) palloc(key->partnatts * sizeof(Datum));
+		boundinfo->isnulls[i] = (bool *) palloc(key->partnatts * sizeof(bool));
+
+
+		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->datums[i] = &boundDatums[i];
-		boundinfo->datums[i][0] = datumCopy(all_values[i].value,
-											key->parttypbyval[0],
-											key->parttyplen[0]);
+			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 +639,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 +672,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 +689,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 +734,7 @@ 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->isnulls = NULL;
 	/* Will be set correctly below. */
 	boundinfo->default_index = -1;
 
@@ -905,6 +937,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,7 +949,7 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 	if (b1->nindexes != b2->nindexes)
 		return false;
 
-	if (b1->null_index != b2->null_index)
+	if (get_partition_bound_null_index(b1) != get_partition_bound_null_index(b2))
 		return false;
 
 	if (b1->default_index != b2->default_index)
@@ -988,7 +1022,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 +1075,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 +1125,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 +1144,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[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 +1219,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 +1264,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)
@@ -1220,6 +1279,8 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 	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);
+	int			outer_null_index = get_partition_bound_null_index(outer_bi);
+	int			inner_null_index = get_partition_bound_null_index(inner_bi);
 	PartitionMap outer_map;
 	PartitionMap inner_map;
 	int			outer_pos;
@@ -1229,6 +1290,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 +1328,34 @@ 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;
+
+		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];
+
+		//TODO: Handle for multi-column case.
+		if (outer_isnull[0] && inner_isnull[0])
+		{
+			outer_pos++;
+			inner_pos++;
+			continue;
+		}
+		else if (outer_isnull[0])
+		{
+			outer_pos++;
+			continue;
+		}
+		else if (inner_isnull[0])
+		{
+			inner_pos++;
+			continue;
+		}
+
 
 		if (outer_pos < outer_bi->ndatums)
 		{
@@ -1316,10 +1406,11 @@ 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]));
+			//TODO: handle multi-column case
+			cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+												outer_datums, outer_isnull,
+												inner_datums, inner_isnull,
+												partnatts);
 		}
 
 		if (cmpval == 0)
@@ -1341,6 +1432,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				goto cleanup;
 
 			merged_datum = outer_datums;
+			merged_isnull = outer_isnull;
 
 			/* Move to the next pair of list values. */
 			outer_pos++;
@@ -1374,6 +1466,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				if (merged_index == -1)
 					goto cleanup;
 				merged_datum = outer_datums;
+				merged_isnull = outer_isnull;
 			}
 
 			/* Move to the next list value on the outer side. */
@@ -1408,6 +1501,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				if (merged_index == -1)
 					goto cleanup;
 				merged_datum = inner_datums;
+				merged_isnull = inner_isnull;
 			}
 
 			/* Move to the next list value on the inner side. */
@@ -1422,6 +1516,7 @@ 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);
 		}
 	}
 
@@ -1430,17 +1525,17 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 	 * non-existent.
 	 */
 	if (outer_has_null &&
-		is_dummy_partition(outer_rel, outer_bi->null_index))
+		is_dummy_partition(outer_rel, outer_null_index))
 		outer_has_null = false;
 	if (inner_has_null &&
-		is_dummy_partition(inner_rel, inner_bi->null_index))
+		is_dummy_partition(inner_rel, inner_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,
+							  outer_null_index, inner_null_index,
 							  jointype, &next_index, &null_index);
 	else
 		Assert(null_index == -1);
@@ -1478,6 +1573,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 +1584,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 +1893,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,
@@ -2527,19 +2625,41 @@ 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);
 	int			pos;
 	ListCell   *lc;
+	int			natts = 1;  //TODO: Handle for multi-column case
+	bool	   *null = NULL;
 
 	merged_bounds = (PartitionBoundInfo) palloc(sizeof(PartitionBoundInfoData));
 	merged_bounds->strategy = strategy;
-	merged_bounds->ndatums = ndatums;
 
+	if (merged_isnulls)
+	{
+		if (null_index >= 0)
+		{
+			null = (bool *) palloc0(sizeof(bool) * natts);
+			null[0] = true;
+			ndatums++;
+		}
+		merged_bounds->isnulls = (bool **) palloc(sizeof(bool *) * ndatums);
+
+		pos = 0;
+		foreach(lc, merged_isnulls)
+			merged_bounds->isnulls[pos++] = (bool *) lfirst(lc);
+
+		if (null_index >= 0)
+			merged_bounds->isnulls[pos] = null;
+	}
+
+	merged_bounds->ndatums = ndatums;
 	merged_bounds->datums = (Datum **) palloc(sizeof(Datum *) * ndatums);
+
 	pos = 0;
 	foreach(lc, merged_datums)
 		merged_bounds->datums[pos++] = (Datum *) lfirst(lc);
@@ -2556,6 +2676,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
 	{
@@ -2564,14 +2685,17 @@ build_merged_partition_bounds(char strategy, List *merged_datums,
 		merged_bounds->kind = 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;
+	if (merged_isnulls && null_index >= 0)
+		merged_bounds->indexes[pos] = null_index;
+
 	merged_bounds->default_index = default_index;
 
 	return merged_bounds;
@@ -3071,32 +3195,36 @@ 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 = 0;
+						int         offset = -1;
+						bool        equal = false;
+						List	   *elem = lfirst(cell);
+						Datum	   *values = (Datum *) palloc0(key->partnatts * sizeof(Datum));
+						bool	   *isnulls = (bool *) palloc0(key->partnatts * sizeof(bool));
+
+						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;
 						}
+
+						pfree(values);
+						pfree(isnulls);
 					}
 				}
 
@@ -3609,6 +3737,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
@@ -3618,8 +3788,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,
@@ -3632,10 +3802,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;
@@ -3805,13 +3975,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 = (*(PartitionListValue *const *) a)->values;
+	Datum	   *vals2 = (*(PartitionListValue *const *) b)->values;
+	bool	   *isnull1 = (*(PartitionListValue *const *) a)->isnulls;
+	bool	   *isnull2 = (*(PartitionListValue *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);
 }
 
 /*
@@ -3911,9 +4083,8 @@ make_partition_op_expr(PartitionKey key, int keynum,
 				int			nelems = list_length(elems);
 
 				Assert(nelems >= 1);
-				Assert(keynum == 0);
 
-				if (nelems > 1 &&
+				if (key->partnatts == 1 && nelems > 1 &&
 					!type_is_array(key->parttypid[keynum]))
 				{
 					ArrayExpr  *arrexpr;
@@ -3942,7 +4113,7 @@ make_partition_op_expr(PartitionKey key, int keynum,
 
 					result = (Expr *) saopexpr;
 				}
-				else
+				else if (key->partnatts == 1)
 				{
 					List	   *elemops = NIL;
 					ListCell   *lc;
@@ -3963,6 +4134,15 @@ make_partition_op_expr(PartitionKey key, int keynum,
 
 					result = nelems > 1 ? makeBoolExpr(OR_EXPR, elemops, -1) : linitial(elemops);
 				}
+				else
+				{
+					result = make_opclause(operoid,
+										   BOOLOID,
+										   false,
+										   arg1, arg2,
+										   InvalidOid,
+										   key->partcollation[keynum]);
+				}
 				break;
 			}
 
@@ -4087,11 +4267,8 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 	List	   *elems = NIL;
 	bool		list_has_null = false;
 
-	/*
-	 * Only single-column list partitioning is supported, so we are worried
-	 * only about the partition key with index 0.
-	 */
-	Assert(key->partnatts == 1);
+	if (key->partnatts > 1)
+		return get_qual_for_multi_column_list(parent, spec);
 
 	/* Construct Var or expression representing the partition column */
 	if (key->partattrs[0] != 0)
@@ -4117,13 +4294,8 @@ 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.
@@ -4135,6 +4307,12 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 		{
 			Const	   *val;
 
+			if (boundinfo->isnulls[i][0])
+			{
+				list_has_null = true;
+				continue;
+			}
+
 			/*
 			 * Construct Const from known-not-null datum.  We must be careful
 			 * to copy the value, because our result has to be able to outlive
@@ -4144,7 +4322,7 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 							key->parttypmod[0],
 							key->parttypcoll[0],
 							key->parttyplen[0],
-							datumCopy(*boundinfo->datums[i],
+							datumCopy(boundinfo->datums[i][0],
 									  key->parttypbyval[0],
 									  key->parttyplen[0]),
 							false,	/* isnull */
@@ -4160,12 +4338,17 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 		 */
 		foreach(cell, spec->listdatums)
 		{
-			Const	   *val = lfirst_node(Const, cell);
+			ListCell	   *cell2 = NULL;
 
-			if (val->constisnull)
-				list_has_null = true;
-			else
-				elems = lappend(elems, copyObject(val));
+			foreach(cell2, (List *) lfirst(cell))
+			{
+				Const      *val = castNode(Const, lfirst(cell2));
+
+				if (val->constisnull)
+					list_has_null = true;
+				else
+					elems = lappend(elems, copyObject(val));
+			}
 		}
 	}
 
@@ -4241,6 +4424,158 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 }
 
 /*
+ * get_qual_for_list_for_multi_column
+ *
+ * Returns a list of expressions to use as a list partition's constraint,
+ * given the parent relation and partition bound structure.
+ *
+ * Returns NIL for a default partition when it's the only partition since
+ * in that case there is no constraint.
+ */
+static List *
+get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
+{
+	int			i = 0;
+	int			j = 0;
+	PartitionKey key = RelationGetPartitionKey(parent);
+	List	   *result;
+	Expr	   *opexpr;
+	NullTest   *nulltest;
+	ListCell   *cell;
+	List	   *elems = NIL;
+	Expr      **keyCol = (Expr **) palloc0 (key->partnatts * sizeof(Expr *));
+
+	/* Construct Var or expression representing the partition columns */
+	for (i = 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;
+		}
+	}
+
+	/*
+	 * For default list partition, collect datums for all the partitions. The
+	 * default partition constraint should check that the partition key is
+	 * equal to none of those.
+	 */
+	if (spec->is_default)
+	{
+		int			ndatums = 0;
+		PartitionDesc pdesc = RelationGetPartitionDesc(parent, false);
+		PartitionBoundInfo boundinfo = pdesc->boundinfo;
+
+		if (boundinfo)
+			ndatums = boundinfo->ndatums;
+
+		/*
+		 * If default is the only partition, there need not be any partition
+		 * constraint on it.
+		 */
+		if (ndatums == 0)
+			return NIL;
+
+		for (i = 0; i < ndatums; i++)
+		{
+			List       *andexpr = NIL;
+
+			for (j = 0; j < key->partnatts; j++)
+			{
+				Const      *val = NULL;
+
+				if (boundinfo->isnulls[i][j])
+				{
+					nulltest = makeNode(NullTest);
+					nulltest->arg = keyCol[j];
+					nulltest->nulltesttype = IS_NULL;
+					nulltest->argisrow = false;
+					nulltest->location = -1;
+					andexpr = lappend(andexpr, 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]);
+
+					opexpr = make_partition_op_expr(key, j, BTEqualStrategyNumber,
+													keyCol[j], (Expr *) val);
+					andexpr = lappend(andexpr, opexpr);
+				}
+			}
+
+			opexpr = makeBoolExpr(AND_EXPR, andexpr, -1);
+			elems = lappend(elems, opexpr);
+		}
+	}
+	else
+	{
+		/*
+		 * Create list of Consts for the allowed values.
+		 */
+		foreach(cell, spec->listdatums)
+		{
+			List	   *andexpr = NIL;
+			ListCell   *cell2 = NULL;
+
+			j = 0;
+			foreach(cell2, (List *) lfirst(cell))
+			{
+				Const      *val = castNode(Const, lfirst(cell2));
+
+				if (val->constisnull)
+				{
+					nulltest = makeNode(NullTest);
+					nulltest->arg = keyCol[j];
+					nulltest->nulltesttype = IS_NULL;
+					nulltest->argisrow = false;
+					nulltest->location = -1;
+					andexpr = lappend(andexpr, nulltest);
+				}
+				else
+				{
+					opexpr = make_partition_op_expr(key, j, BTEqualStrategyNumber,
+													keyCol[j], (Expr *) val);
+					andexpr = lappend(andexpr, opexpr);
+				}
+				j++;
+			}
+
+			opexpr = makeBoolExpr(AND_EXPR, andexpr, -1);
+			elems = lappend(elems, opexpr);
+		}
+	}
+
+	opexpr = makeBoolExpr(OR_EXPR, elems, -1);
+	result = list_make1(opexpr);
+
+	/*
+	 * Note that, in general, applying NOT to a constraint expression doesn't
+	 * necessarily invert the set of rows it accepts, because NOT (NULL) is
+	 * NULL.  However, the partition constraints we construct here never
+	 * evaluate to NULL, so applying NOT works as intended.
+	 */
+	if (spec->is_default)
+		result = list_make1(makeBoolExpr(NOT_EXPR, result, -1));
+
+	return result;
+}
+
+/*
  * get_qual_for_range
  *
  * Returns an implicit-AND list of expressions to use as a range partition's
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index e00edbe..76ea26c 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;
 
 /*
@@ -185,8 +187,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);
@@ -908,7 +910,8 @@ get_matching_partitions(PartitionPruneContext *context, List *pruning_steps)
 	{
 		Assert(context->strategy == PARTITION_STRATEGY_LIST);
 		Assert(partition_bound_accepts_nulls(context->boundinfo));
-		result = bms_add_member(result, context->boundinfo->null_index);
+		result = bms_add_member(result,
+								get_partition_bound_null_index(context->boundinfo));
 	}
 	if (scan_default)
 	{
@@ -1229,14 +1232,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 +1246,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 +1396,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 +1421,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 +2331,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;
 
-		return PARTCLAUSE_MATCH_NULLNESS;
+			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;
+			}
+
+			*pc = partclause;
+			return PARTCLAUSE_MATCH_CLAUSE;
+		}
 	}
 
 	/*
@@ -2637,6 +2671,169 @@ get_matching_hash_bounds(PartitionPruneContext *context,
 }
 
 /*
+ * 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;
+
+	/*
+	 * 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.
+	 */
+	if (off >= 0)
+	{
+		if (is_equal && nvalues < partnatts)
+		{
+			while (off >= 1 && off < boundinfo->ndatums - 1)
+			{
+				int32       cmpval;
+				int         nextoff;
+
+				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 = 0;
+	int j = 0;
+
+	Assert(minoff >= 0 && maxoff >= 0 && ncols > 0);
+	for (i = minoff; i < maxoff; i++)
+	{
+		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);
+	}
+}
+
+/*
  * get_matching_list_bounds
  *		Determine the offsets of list bounds matching the specified value,
  *		according to the semantics of the given operator strategy
@@ -2658,8 +2855,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,26 +2866,12 @@ 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,
 	 * just return the default partition if one exists.
@@ -2700,7 +2883,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,32 +2892,49 @@ 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;
 	}
 
 	/* Special case handling of values coming from a <> operator clause. */
 	if (opstrategy == InvalidStrategy)
 	{
+		int i = 0;
+
 		/*
 		 * 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
+			{
+				/*
+				 * 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);
 
-			/* We have a match. Remove from the result. */
-			Assert(boundinfo->indexes[off] >= 0);
-			result->bound_offsets = bms_del_member(result->bound_offsets,
-												   off);
+				/* 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 +2957,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 +3021,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 +3050,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;
 }
 
@@ -3343,6 +3559,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 +3581,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,10 +3608,11 @@ 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;
 
@@ -3405,26 +3625,35 @@ perform_pruning_base_step(PartitionPruneContext *context,
 			}
 
 			/* 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 +3680,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);
 
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 4df8cc5..75e431d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9407,10 +9407,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 = ", ";
 						}
 
@@ -11971,6 +11970,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 = NULL;
+	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 9db546d..25f61c3 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 booleans 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,6 +80,7 @@ typedef struct PartitionBoundInfoData
 	char		strategy;		/* hash, list or range? */
 	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 */
@@ -87,15 +89,15 @@ 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_partition_bound_null_index(PartitionBoundInfo boundinfo);
+
 extern int	get_hash_partition_greatest_modulus(PartitionBoundInfo b);
 extern uint64 compute_partition_hash_value(int partnatts, FmgrInfo *partsupfunc,
 										   Oid *partcollation,
@@ -130,15 +132,19 @@ 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,
 										  int nvalues, Datum *values, bool *is_equal);
 extern int	partition_hash_bsearch(PartitionBoundInfo boundinfo,
 								   int modulus, int remainder);
-
 #endif							/* PARTBOUNDS_H */
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 96bf426..89f1cb8 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,
@@ -919,6 +913,34 @@ 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;
+-- 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..8e704d3 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 = '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 = '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_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..61e1129 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,
@@ -728,6 +722,25 @@ 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;
+
+-- 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_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

#13Zhihong Yu
zyu@yugabyte.com
In reply to: Nitin Jadhav (#12)
Re: Multi-Column List Partitioning

On Wed, Aug 25, 2021 at 5:41 AM Nitin Jadhav <nitinjadhavpostgres@gmail.com>
wrote:

The new list bound binary search and related comparison support
function look a bit too verbose to me. I was expecting
partition_list_bsearch() to look very much like
partition_range_datum_bsearch(), but that is not the case. The
special case code that you wrote in partition_list_bsearch() seems
unnecessary, at least in that function. I'm talking about the code
fragment starting with this comment:

I will look at other parts of the patch next week hopefully. For
now, attached is a delta patch that applies on top of your v1, which
does:

* Simplify partition_list_bsearch() and partition_lbound_datum_cmp()
* Make qsort_partition_list_value_cmp simply call
partition_lbound_datum_cmp() instead of having its own logic to
compare input bounds
* Move partition_lbound_datum_cmp() into partbounds.c as a static
function (export seems unnecessary)
* Add a comment for PartitionBoundInfo.isnulls and remove that for

null_index

Yes. You are right. The extra code added in partition_list_bsearch()
is not required and thanks for sharing the delta patch. It looks good
to me and I have incorporated the changes in the attached patch.

I guess you're perhaps trying to address the case where the caller
does not specify the values for all of the partition key columns,
which can happen when the partition pruning code needs to handle a set
of clauses matching only some of the partition key columns. But
that's a concern of the partition pruning code and so the special case
should be handled there (if at all), not in the binary search function
that is shared with other callers. Regarding that, I'm wondering if
we should require clauses matching all of the partition key columns to
be found for the pruning code to call the binary search, so do
something like get_matching_hash_bounds() does:

Do you think that trying to match list partitions even with fewer keys
is worth the complexity of the implementation? That is, is the use
case to search for only a subset of partition key columns common
enough with list partitioning?

If we do decide to implement the special case, remember that to do
that efficiently, we'd need to require that the subset of matched key
columns constitutes a prefix, because of the way the datums are
sorted. That is, match all partitions when the query only contains a
clause for b when the partition key is (a, b, c), but engage the
special case of pruning if the query contains clauses for a, or for a
and b.

Thanks for the suggestion. Below is the implementation details for the
partition pruning for multi column list partitioning.

In the existing code (For single column list partitioning)
1. In gen_partprune_steps_internal(), we try to match the where
clauses provided by the user with the partition key data using
match_clause_to_partition_key(). Based on the match, this function can
return many values like PARTCLAUSE_MATCH_CLAUSE,
PARTCLAUSE_MATCH_NULLNESS, PARTCLAUSE_NOMATCH, etc.
2. In case of PARTCLAUSE_MATCH_CLAUSE, we generate steps using
gen_prune_steps_from_opexps() (strategy-2) which generate and return a
list of PartitionPruneStepOp that are based on OpExpr and BooleanTest
clauses that have been matched to the partition key and it also takes
care handling prefix of the partition keys.
3. In case of PARTCLAUSE_MATCH_NULLNESS, we generate steps using
gen_prune_step_op() (strategy-1) which generates single
PartitionPruneStepOp since the earlier list partitioning supports
single column and there can be only one NULL value. In
get_matching_list_bounds(), if the nullkeys is not empty, we fetch the
partition index which accepts null and we used to return from here.

In case of multi column list partitioning, we have columns more than
one and hence there is a possibility of more than one NULL values in
the where clauses. The above mentioned steps are modified like below.

1. Modified the match_clause_to_partition_key() to generate an object
of PartClauseInfo structure and return PARTCLAUSE_MATCH_CLAUSE even in
case of clauses related to NULL. The information required to generate
PartClauseInfo is populated here like the constant expression
consisting of (Datum) 0, op_strategy, op_is_ne, etc.
2. Since I am returning PARTCLAUSE_MATCH_CLAUSE, now we use strategy-2
(gen_prune_steps_from_opexps) to generate partition pruning steps.
This function takes care of generating a list of pruning steps if
there are multiple clauses and also takes care of handling prefixes.
3. Modified perform_pruning_base_step() to generate the datum values
and isnulls data of the where clauses. In case if any of the key
contains NULL value then the corresponding datum value is 0.
4. Modified get_matching_list_bounds() to generate the minimum offset
and/or maximum offset of the matched values based on the difference
operation strategies. Now since the NULL containing bound values are
part of 'boundinfo', changed the code accordingly to include the NULL
containing partitions or not in different scenarios like
InvalidStrategy, etc.

I have done some cosmetic changes to
v1_multi_column_list_partitioning.patch. So all the above code changes
related to partition pruning are merged with the previous patch and
also included the delta patch shared by you. Hence sharing a single
patch.

Kindly have a look and share your thoughts.

Hi,

bq. Supported new syantx to allow mentioning multiple key information.

syantx -> syntax

+       isDuplicate = checkForDuplicates(result, values);
+       if (isDuplicate)
+           continue;

It seems the variable isDuplicate is not needed. The if statement can
directly check the return value from checkForDuplicates().

+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)

Is this part going to be updated in the next patch?

Cheers

#14Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Zhihong Yu (#13)
Re: Multi-Column List Partitioning
+ * isnulls is an array of boolean-tuples with key->partnatts booleans values
+ * each.  Currently only used for list partitioning, it stores whether a

I think 'booleans' should be 'boolean'.
The trailing word 'each' is unnecessary.

bq. Supported new syantx to allow mentioning multiple key information.

syantx -> syntax

+       isDuplicate = checkForDuplicates(result, values);
+       if (isDuplicate)
+           continue;

It seems the variable isDuplicate is not needed. The if statement can directly check the return value from checkForDuplicates().

I agree that isDuplicate is not required.
Thanks for sharing the comments. I will take care of these comments in
the next patch.

+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)

Is this part going to be updated in the next patch?

Yes. The code changes related to partition-wise join are in progress.
I will handle these in the next patch.

Thanks & Regards,
Nitin Jadhav

Show quoted text

On Thu, Aug 26, 2021 at 2:40 AM Zhihong Yu <zyu@yugabyte.com> wrote:

On Wed, Aug 25, 2021 at 5:41 AM Nitin Jadhav <nitinjadhavpostgres@gmail.com> wrote:

The new list bound binary search and related comparison support
function look a bit too verbose to me. I was expecting
partition_list_bsearch() to look very much like
partition_range_datum_bsearch(), but that is not the case. The
special case code that you wrote in partition_list_bsearch() seems
unnecessary, at least in that function. I'm talking about the code
fragment starting with this comment:

I will look at other parts of the patch next week hopefully. For
now, attached is a delta patch that applies on top of your v1, which
does:

* Simplify partition_list_bsearch() and partition_lbound_datum_cmp()
* Make qsort_partition_list_value_cmp simply call
partition_lbound_datum_cmp() instead of having its own logic to
compare input bounds
* Move partition_lbound_datum_cmp() into partbounds.c as a static
function (export seems unnecessary)
* Add a comment for PartitionBoundInfo.isnulls and remove that for null_index

Yes. You are right. The extra code added in partition_list_bsearch()
is not required and thanks for sharing the delta patch. It looks good
to me and I have incorporated the changes in the attached patch.

I guess you're perhaps trying to address the case where the caller
does not specify the values for all of the partition key columns,
which can happen when the partition pruning code needs to handle a set
of clauses matching only some of the partition key columns. But
that's a concern of the partition pruning code and so the special case
should be handled there (if at all), not in the binary search function
that is shared with other callers. Regarding that, I'm wondering if
we should require clauses matching all of the partition key columns to
be found for the pruning code to call the binary search, so do
something like get_matching_hash_bounds() does:

Do you think that trying to match list partitions even with fewer keys
is worth the complexity of the implementation? That is, is the use
case to search for only a subset of partition key columns common
enough with list partitioning?

If we do decide to implement the special case, remember that to do
that efficiently, we'd need to require that the subset of matched key
columns constitutes a prefix, because of the way the datums are
sorted. That is, match all partitions when the query only contains a
clause for b when the partition key is (a, b, c), but engage the
special case of pruning if the query contains clauses for a, or for a
and b.

Thanks for the suggestion. Below is the implementation details for the
partition pruning for multi column list partitioning.

In the existing code (For single column list partitioning)
1. In gen_partprune_steps_internal(), we try to match the where
clauses provided by the user with the partition key data using
match_clause_to_partition_key(). Based on the match, this function can
return many values like PARTCLAUSE_MATCH_CLAUSE,
PARTCLAUSE_MATCH_NULLNESS, PARTCLAUSE_NOMATCH, etc.
2. In case of PARTCLAUSE_MATCH_CLAUSE, we generate steps using
gen_prune_steps_from_opexps() (strategy-2) which generate and return a
list of PartitionPruneStepOp that are based on OpExpr and BooleanTest
clauses that have been matched to the partition key and it also takes
care handling prefix of the partition keys.
3. In case of PARTCLAUSE_MATCH_NULLNESS, we generate steps using
gen_prune_step_op() (strategy-1) which generates single
PartitionPruneStepOp since the earlier list partitioning supports
single column and there can be only one NULL value. In
get_matching_list_bounds(), if the nullkeys is not empty, we fetch the
partition index which accepts null and we used to return from here.

In case of multi column list partitioning, we have columns more than
one and hence there is a possibility of more than one NULL values in
the where clauses. The above mentioned steps are modified like below.

1. Modified the match_clause_to_partition_key() to generate an object
of PartClauseInfo structure and return PARTCLAUSE_MATCH_CLAUSE even in
case of clauses related to NULL. The information required to generate
PartClauseInfo is populated here like the constant expression
consisting of (Datum) 0, op_strategy, op_is_ne, etc.
2. Since I am returning PARTCLAUSE_MATCH_CLAUSE, now we use strategy-2
(gen_prune_steps_from_opexps) to generate partition pruning steps.
This function takes care of generating a list of pruning steps if
there are multiple clauses and also takes care of handling prefixes.
3. Modified perform_pruning_base_step() to generate the datum values
and isnulls data of the where clauses. In case if any of the key
contains NULL value then the corresponding datum value is 0.
4. Modified get_matching_list_bounds() to generate the minimum offset
and/or maximum offset of the matched values based on the difference
operation strategies. Now since the NULL containing bound values are
part of 'boundinfo', changed the code accordingly to include the NULL
containing partitions or not in different scenarios like
InvalidStrategy, etc.

I have done some cosmetic changes to
v1_multi_column_list_partitioning.patch. So all the above code changes
related to partition pruning are merged with the previous patch and
also included the delta patch shared by you. Hence sharing a single
patch.

Kindly have a look and share your thoughts.

Hi,

bq. Supported new syantx to allow mentioning multiple key information.

syantx -> syntax

+       isDuplicate = checkForDuplicates(result, values);
+       if (isDuplicate)
+           continue;

It seems the variable isDuplicate is not needed. The if statement can directly check the return value from checkForDuplicates().

+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)

Is this part going to be updated in the next patch?

Cheers

#15Rajkumar Raghuwanshi
rajkumar.raghuwanshi@enterprisedb.com
In reply to: Nitin Jadhav (#14)
Re: Multi-Column List Partitioning

Hi Nitin.

I have been testing these patches. Patches applied cleanly on the head.
While testing I found below a case where update row movement is not working
properly.
Please find the test case below.

postgres=# create table p0 (a int, b text, c bool) partition by list
(a,b,c);
CREATE TABLE
postgres=# create table p01 partition of p0 for values in ((1,1,true));
CREATE TABLE
postgres=# create table p02 partition of p0 for values in ((1,NULL,false));
CREATE TABLE
postgres=# insert into p0 values (1,'1',true);
INSERT 0 1
postgres=# insert into p0 values (1,NULL,false);
INSERT 0 1
postgres=# select tableoid::regclass,* from p0;
tableoid | a | b | c
----------+---+---+---
p01 | 1 | 1 | t
p02 | 1 | | f
(2 rows)

postgres=# update p0 set b = NULL;
UPDATE 2
postgres=# select tableoid::regclass,* from p0;
tableoid | a | b | c
----------+---+---+---
p01 | 1 | | t
p02 | 1 | | f
(2 rows)

I think this update should fail as there is no partition satisfying update
row (1,NULL,true).

Thanks & Regards,
Rajkumar Raghuwanshi

On Fri, Aug 27, 2021 at 12:53 PM Nitin Jadhav <nitinjadhavpostgres@gmail.com>
wrote:

Show quoted text

+ * isnulls is an array of boolean-tuples with key->partnatts booleans

values

+ * each. Currently only used for list partitioning, it stores whether a

I think 'booleans' should be 'boolean'.
The trailing word 'each' is unnecessary.

bq. Supported new syantx to allow mentioning multiple key information.

syantx -> syntax

+       isDuplicate = checkForDuplicates(result, values);
+       if (isDuplicate)
+           continue;

It seems the variable isDuplicate is not needed. The if statement can

directly check the return value from checkForDuplicates().

I agree that isDuplicate is not required.
Thanks for sharing the comments. I will take care of these comments in
the next patch.

+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)

Is this part going to be updated in the next patch?

Yes. The code changes related to partition-wise join are in progress.
I will handle these in the next patch.

Thanks & Regards,
Nitin Jadhav

On Thu, Aug 26, 2021 at 2:40 AM Zhihong Yu <zyu@yugabyte.com> wrote:

On Wed, Aug 25, 2021 at 5:41 AM Nitin Jadhav <

nitinjadhavpostgres@gmail.com> wrote:

The new list bound binary search and related comparison support
function look a bit too verbose to me. I was expecting
partition_list_bsearch() to look very much like
partition_range_datum_bsearch(), but that is not the case. The
special case code that you wrote in partition_list_bsearch() seems
unnecessary, at least in that function. I'm talking about the code
fragment starting with this comment:

I will look at other parts of the patch next week hopefully. For
now, attached is a delta patch that applies on top of your v1, which
does:

* Simplify partition_list_bsearch() and partition_lbound_datum_cmp()
* Make qsort_partition_list_value_cmp simply call
partition_lbound_datum_cmp() instead of having its own logic to
compare input bounds
* Move partition_lbound_datum_cmp() into partbounds.c as a static
function (export seems unnecessary)
* Add a comment for PartitionBoundInfo.isnulls and remove that for

null_index

Yes. You are right. The extra code added in partition_list_bsearch()
is not required and thanks for sharing the delta patch. It looks good
to me and I have incorporated the changes in the attached patch.

I guess you're perhaps trying to address the case where the caller
does not specify the values for all of the partition key columns,
which can happen when the partition pruning code needs to handle a set
of clauses matching only some of the partition key columns. But
that's a concern of the partition pruning code and so the special case
should be handled there (if at all), not in the binary search function
that is shared with other callers. Regarding that, I'm wondering if
we should require clauses matching all of the partition key columns to
be found for the pruning code to call the binary search, so do
something like get_matching_hash_bounds() does:

Do you think that trying to match list partitions even with fewer keys
is worth the complexity of the implementation? That is, is the use
case to search for only a subset of partition key columns common
enough with list partitioning?

If we do decide to implement the special case, remember that to do
that efficiently, we'd need to require that the subset of matched key
columns constitutes a prefix, because of the way the datums are
sorted. That is, match all partitions when the query only contains a
clause for b when the partition key is (a, b, c), but engage the
special case of pruning if the query contains clauses for a, or for a
and b.

Thanks for the suggestion. Below is the implementation details for the
partition pruning for multi column list partitioning.

In the existing code (For single column list partitioning)
1. In gen_partprune_steps_internal(), we try to match the where
clauses provided by the user with the partition key data using
match_clause_to_partition_key(). Based on the match, this function can
return many values like PARTCLAUSE_MATCH_CLAUSE,
PARTCLAUSE_MATCH_NULLNESS, PARTCLAUSE_NOMATCH, etc.
2. In case of PARTCLAUSE_MATCH_CLAUSE, we generate steps using
gen_prune_steps_from_opexps() (strategy-2) which generate and return a
list of PartitionPruneStepOp that are based on OpExpr and BooleanTest
clauses that have been matched to the partition key and it also takes
care handling prefix of the partition keys.
3. In case of PARTCLAUSE_MATCH_NULLNESS, we generate steps using
gen_prune_step_op() (strategy-1) which generates single
PartitionPruneStepOp since the earlier list partitioning supports
single column and there can be only one NULL value. In
get_matching_list_bounds(), if the nullkeys is not empty, we fetch the
partition index which accepts null and we used to return from here.

In case of multi column list partitioning, we have columns more than
one and hence there is a possibility of more than one NULL values in
the where clauses. The above mentioned steps are modified like below.

1. Modified the match_clause_to_partition_key() to generate an object
of PartClauseInfo structure and return PARTCLAUSE_MATCH_CLAUSE even in
case of clauses related to NULL. The information required to generate
PartClauseInfo is populated here like the constant expression
consisting of (Datum) 0, op_strategy, op_is_ne, etc.
2. Since I am returning PARTCLAUSE_MATCH_CLAUSE, now we use strategy-2
(gen_prune_steps_from_opexps) to generate partition pruning steps.
This function takes care of generating a list of pruning steps if
there are multiple clauses and also takes care of handling prefixes.
3. Modified perform_pruning_base_step() to generate the datum values
and isnulls data of the where clauses. In case if any of the key
contains NULL value then the corresponding datum value is 0.
4. Modified get_matching_list_bounds() to generate the minimum offset
and/or maximum offset of the matched values based on the difference
operation strategies. Now since the NULL containing bound values are
part of 'boundinfo', changed the code accordingly to include the NULL
containing partitions or not in different scenarios like
InvalidStrategy, etc.

I have done some cosmetic changes to
v1_multi_column_list_partitioning.patch. So all the above code changes
related to partition pruning are merged with the previous patch and
also included the delta patch shared by you. Hence sharing a single
patch.

Kindly have a look and share your thoughts.

Hi,

bq. Supported new syantx to allow mentioning multiple key information.

syantx -> syntax

+       isDuplicate = checkForDuplicates(result, values);
+       if (isDuplicate)
+           continue;

It seems the variable isDuplicate is not needed. The if statement can

directly check the return value from checkForDuplicates().

+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)

Is this part going to be updated in the next patch?

Cheers

#16Amit Langote
amitlangote09@gmail.com
In reply to: Rajkumar Raghuwanshi (#15)
Re: Multi-Column List Partitioning

On Mon, Aug 30, 2021 at 4:51 PM Rajkumar Raghuwanshi
<rajkumar.raghuwanshi@enterprisedb.com> wrote:

Hi Nitin.

I have been testing these patches. Patches applied cleanly on the head. While testing I found below a case where update row movement is not working properly.
Please find the test case below.

postgres=# create table p0 (a int, b text, c bool) partition by list (a,b,c);
CREATE TABLE
postgres=# create table p01 partition of p0 for values in ((1,1,true));
CREATE TABLE
postgres=# create table p02 partition of p0 for values in ((1,NULL,false));
CREATE TABLE
postgres=# insert into p0 values (1,'1',true);
INSERT 0 1
postgres=# insert into p0 values (1,NULL,false);
INSERT 0 1
postgres=# select tableoid::regclass,* from p0;
tableoid | a | b | c
----------+---+---+---
p01 | 1 | 1 | t
p02 | 1 | | f
(2 rows)

postgres=# update p0 set b = NULL;
UPDATE 2
postgres=# select tableoid::regclass,* from p0;
tableoid | a | b | c
----------+---+---+---
p01 | 1 | | t
p02 | 1 | | f
(2 rows)

I think this update should fail as there is no partition satisfying update row (1,NULL,true).

Yeah, contrary to my earlier assessment, it seems the partition
constraint on each of those partitions fails to explicitly include an
IS NOT NULL test for each column that has a non-NULL value assigned.
So, for example, the constraint of p01 should actually be:

(a IS NOT NULL) AND (a = 1) AND (b IS NOT NULL) AND (b = 1) AND (c IS
NOT NULL) AND (c = true)

As per the patch's current implementation, tuple (1, NULL, true)
passes p01's partition constraint, because only (b = 1) is not
sufficient to reject a NULL value being assigned to b.

--
Amit Langote
EDB: http://www.enterprisedb.com

#17Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Amit Langote (#16)
1 attachment(s)
Re: Multi-Column List Partitioning

I have been testing these patches. Patches applied cleanly on the head. While testing I found below a case where update row movement is not working properly.
Please find the test case below.

Thanks for testing and sharing the details of the issue.

Yeah, contrary to my earlier assessment, it seems the partition
constraint on each of those partitions fails to explicitly include an
IS NOT NULL test for each column that has a non-NULL value assigned.
So, for example, the constraint of p01 should actually be:

(a IS NOT NULL) AND (a = 1) AND (b IS NOT NULL) AND (b = 1) AND (c IS
NOT NULL) AND (c = true)

Yes. It should add an IS NOT NULL test for each column. I have
modified the patch accordingly and verified with the test case shared
by Rajkumar.

+ * isnulls is an array of boolean-tuples with key->partnatts booleans values
+ * each.  Currently only used for list partitioning, it stores whether a

I think 'booleans' should be 'boolean'.
The trailing word 'each' is unnecessary.

bq. Supported new syantx to allow mentioning multiple key information.

syantx -> syntax

+       isDuplicate = checkForDuplicates(result, values);
+       if (isDuplicate)
+           continue;

It seems the variable isDuplicate is not needed. The if statement can directly check the return value from checkForDuplicates().

The attached patch also fixes the above comments.

Thanks & Regards,
Nitin Jadhav

Show quoted text

On Tue, Aug 31, 2021 at 9:36 AM Amit Langote <amitlangote09@gmail.com> wrote:

On Mon, Aug 30, 2021 at 4:51 PM Rajkumar Raghuwanshi
<rajkumar.raghuwanshi@enterprisedb.com> wrote:

Hi Nitin.

I have been testing these patches. Patches applied cleanly on the head. While testing I found below a case where update row movement is not working properly.
Please find the test case below.

postgres=# create table p0 (a int, b text, c bool) partition by list (a,b,c);
CREATE TABLE
postgres=# create table p01 partition of p0 for values in ((1,1,true));
CREATE TABLE
postgres=# create table p02 partition of p0 for values in ((1,NULL,false));
CREATE TABLE
postgres=# insert into p0 values (1,'1',true);
INSERT 0 1
postgres=# insert into p0 values (1,NULL,false);
INSERT 0 1
postgres=# select tableoid::regclass,* from p0;
tableoid | a | b | c
----------+---+---+---
p01 | 1 | 1 | t
p02 | 1 | | f
(2 rows)

postgres=# update p0 set b = NULL;
UPDATE 2
postgres=# select tableoid::regclass,* from p0;
tableoid | a | b | c
----------+---+---+---
p01 | 1 | | t
p02 | 1 | | f
(2 rows)

I think this update should fail as there is no partition satisfying update row (1,NULL,true).

Yeah, contrary to my earlier assessment, it seems the partition
constraint on each of those partitions fails to explicitly include an
IS NOT NULL test for each column that has a non-NULL value assigned.
So, for example, the constraint of p01 should actually be:

(a IS NOT NULL) AND (a = 1) AND (b IS NOT NULL) AND (b = 1) AND (c IS
NOT NULL) AND (c = true)

As per the patch's current implementation, tuple (1, NULL, true)
passes p01's partition constraint, because only (b = 1) is not
sufficient to reject a NULL value being assigned to b.

--
Amit Langote
EDB: http://www.enterprisedb.com

Attachments:

v3-0001-multi-column-list-partitioning.patchapplication/x-patch; name=v3-0001-multi-column-list-partitioning.patchDownload
From 35e386f9f8ecbadbaa20f3138c5e543cd6050d53 Mon Sep 17 00:00:00 2001
From: Nitin <nitin.jadhav@enterprisedb.com>
Date: Tue, 31 Aug 2021 13:29:10 +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 accomodate multiple NULL values in
case of list partitioning. Supported partition pruning mechanism
to work for multiple keys.
---
 src/backend/commands/tablecmds.c              |   7 -
 src/backend/executor/execPartition.c          |  10 +-
 src/backend/parser/parse_utilcmd.c            | 196 ++++++--
 src/backend/partitioning/partbounds.c         | 649 ++++++++++++++++++++------
 src/backend/partitioning/partprune.c          | 439 ++++++++++++-----
 src/backend/utils/adt/ruleutils.c             |  45 +-
 src/include/partitioning/partbounds.h         |  22 +-
 src/include/utils/ruleutils.h                 |   1 +
 src/test/regress/expected/create_table.out    |  34 +-
 src/test/regress/expected/insert.out          | 147 ++++++
 src/test/regress/expected/partition_prune.out | 432 +++++++++++++++++
 src/test/regress/sql/create_table.sql         |  25 +-
 src/test/regress/sql/insert.sql               |  64 +++
 src/test/regress/sql/partition_prune.sql      |  42 ++
 14 files changed, 1774 insertions(+), 339 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index dbee6ae..4df8789 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 e5eefdb..c37cc10 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)
 }
 
 /*
+ * checkForDuplicates
+ *
+ * Returns TRUE if the list bound element is already present in the list of
+ * list bounds, FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)
+{
+	ListCell   *cell = NULL;
+
+	foreach(cell, source)
+	{
+		int		i = 0;
+		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(searchElem, 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,110 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 }
 
 /*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw grammar
+ * representation.
+ */
+static List *
+transformPartitionListBounds(ParseState *pstate, PartitionBoundSpec *spec,
+							 Relation parent)
+{
+	int				i = 0;
+	int				j = 0;
+	ListCell	   *cell = NULL;
+	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] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+			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 (partnatts == 1)
+		{
+			Const	   *val =
+				transformPartitionBoundValue(pstate, expr,colname[0],
+											 coltype[0], coltypmod[0],
+											 partcollation[0]);
+			values = lappend(values, val);
+		}
+		else
+		{
+			ListCell   *cell2 = NULL;
+			RowExpr		*rowexpr = NULL;
+
+			if (!IsA(expr, RowExpr))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("Invalid list bound specification"),
+						parser_errposition(pstate, exprLocation((Node *) spec))));
+
+			rowexpr = (RowExpr *) expr;
+			if (partnatts != list_length(rowexpr->args))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("Must specify exactly one value per partitioning column"),
+						 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 (checkForDuplicates(result, values))
+			continue;
+
+		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 fdfe712..f4d3087 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -53,11 +53,12 @@ typedef struct PartitionHashBound
 	int			index;
 } PartitionHashBound;
 
-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
 typedef struct PartitionListValue
 {
 	int			index;
-	Datum		value;
+	Datum	   *values;
+	bool	   *isnulls;
 } PartitionListValue;
 
 /* One bound of a range partition */
@@ -102,7 +103,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,
@@ -175,6 +177,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,
@@ -230,6 +233,7 @@ static Oid	get_partition_operator(PartitionKey key, int col,
 								   StrategyNumber strategy, bool *need_relabel);
 static List *get_qual_for_hash(Relation parent, PartitionBoundSpec *spec);
 static List *get_qual_for_list(Relation parent, PartitionBoundSpec *spec);
+static List *get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec);
 static List *get_qual_for_range(Relation parent, PartitionBoundSpec *spec,
 								bool for_default);
 static void get_range_key_properties(PartitionKey key, int keynum,
@@ -366,7 +370,7 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
 	/* No special hash partitions. */
-	boundinfo->null_index = -1;
+	boundinfo->isnulls = NULL;
 	boundinfo->default_index = -1;
 
 	hbounds = (PartitionHashBound *)
@@ -438,28 +442,72 @@ 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 partition bound has 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 = 0;
+	int j = 0;
 
-	for (i = 0; i < nparts; i++)
-	{
-		ListCell   *lc;
+	if (!boundinfo->isnulls)
+		return false;
 
-		foreach(lc, boundspecs[i]->listdatums)
+	for (i = 0; i < boundinfo->ndatums; i++)
+	{
+		//TODO: Handle for multi-column cases
+		for (j = 0; j < 1; j++)
 		{
-			Const	   *val = lfirst_node(Const, lc);
+			if (boundinfo->isnulls[i][j])
+				return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * get_partition_bound_null_index
+ *
+ * Returns the partition index of the partition bound which accepts NULL.
+ */
+int
+get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+{
+	int i = 0;
+	int j = 0;
+
+	if (!boundinfo->isnulls)
+		return -1;
 
-			if (!val->constisnull)
-				count++;
+	for (i = 0; i < boundinfo->ndatums; i++)
+	{
+		//TODO: Handle for multi-column cases
+		for (j = 0; j < 1; j++)
+		{
+			if (boundinfo->isnulls[i][j])
+				return boundinfo->indexes[i];
 		}
 	}
 
+	return -1;
+}
+
+/*
+ * get_list_datum_count
+ * 		Counts the number of Datums in each partition.
+ */
+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 +520,23 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 				   PartitionKey key, int **mapping)
 {
 	PartitionBoundInfo boundinfo;
-	PartitionListValue *all_values;
+	PartitionListValue **all_values;
 	int			i;
 	int			j;
 	int			ndatums;
 	int			next_index = 0;
 	int			default_index = -1;
-	int			null_index = -1;
 	Datum	   *boundDatums;
 
 	boundinfo = (PartitionBoundInfoData *)
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
 	/* 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 = (PartitionListValue **)
+		palloc(ndatums * sizeof(PartitionListValue *));
 
 	/* Create a unified list of non-null values across all partitions. */
 	for (j = 0, i = 0; i < nparts; i++)
@@ -514,35 +560,40 @@ 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 = NULL;
 
-			if (!val->constisnull)
-			{
-				all_values[j].index = i;
-				all_values[j].value = val->constvalue;
-				j++;
-			}
-			else
+			all_values[j] = (PartitionListValue *) palloc(sizeof(PartitionListValue));
+			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(PartitionListValue *),
 			  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;
@@ -563,12 +614,21 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	 */
 	for (i = 0; i < ndatums; i++)
 	{
-		int			orig_index = all_values[i].index;
+		int         j = 0;
+		int			orig_index = all_values[i]->index;
+		boundinfo->datums[i] = (Datum *) palloc(key->partnatts * sizeof(Datum));
+		boundinfo->isnulls[i] = (bool *) palloc(key->partnatts * sizeof(bool));
+
+
+		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->datums[i] = &boundDatums[i];
-		boundinfo->datums[i][0] = datumCopy(all_values[i].value,
-											key->parttypbyval[0],
-											key->parttyplen[0]);
+			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 +639,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 +672,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 +689,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 +734,7 @@ 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->isnulls = NULL;
 	/* Will be set correctly below. */
 	boundinfo->default_index = -1;
 
@@ -905,6 +937,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,7 +949,7 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 	if (b1->nindexes != b2->nindexes)
 		return false;
 
-	if (b1->null_index != b2->null_index)
+	if (get_partition_bound_null_index(b1) != get_partition_bound_null_index(b2))
 		return false;
 
 	if (b1->default_index != b2->default_index)
@@ -988,7 +1022,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 +1075,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 +1125,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 +1144,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[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 +1219,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 +1264,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)
@@ -1220,6 +1279,8 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 	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);
+	int			outer_null_index = get_partition_bound_null_index(outer_bi);
+	int			inner_null_index = get_partition_bound_null_index(inner_bi);
 	PartitionMap outer_map;
 	PartitionMap inner_map;
 	int			outer_pos;
@@ -1229,6 +1290,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 +1328,34 @@ 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;
+
+		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];
+
+		//TODO: Handle for multi-column case.
+		if (outer_isnull[0] && inner_isnull[0])
+		{
+			outer_pos++;
+			inner_pos++;
+			continue;
+		}
+		else if (outer_isnull[0])
+		{
+			outer_pos++;
+			continue;
+		}
+		else if (inner_isnull[0])
+		{
+			inner_pos++;
+			continue;
+		}
+
 
 		if (outer_pos < outer_bi->ndatums)
 		{
@@ -1316,10 +1406,11 @@ 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]));
+			//TODO: handle multi-column case
+			cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+												outer_datums, outer_isnull,
+												inner_datums, inner_isnull,
+												partnatts);
 		}
 
 		if (cmpval == 0)
@@ -1341,6 +1432,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				goto cleanup;
 
 			merged_datum = outer_datums;
+			merged_isnull = outer_isnull;
 
 			/* Move to the next pair of list values. */
 			outer_pos++;
@@ -1374,6 +1466,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				if (merged_index == -1)
 					goto cleanup;
 				merged_datum = outer_datums;
+				merged_isnull = outer_isnull;
 			}
 
 			/* Move to the next list value on the outer side. */
@@ -1408,6 +1501,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				if (merged_index == -1)
 					goto cleanup;
 				merged_datum = inner_datums;
+				merged_isnull = inner_isnull;
 			}
 
 			/* Move to the next list value on the inner side. */
@@ -1422,6 +1516,7 @@ 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);
 		}
 	}
 
@@ -1430,17 +1525,17 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 	 * non-existent.
 	 */
 	if (outer_has_null &&
-		is_dummy_partition(outer_rel, outer_bi->null_index))
+		is_dummy_partition(outer_rel, outer_null_index))
 		outer_has_null = false;
 	if (inner_has_null &&
-		is_dummy_partition(inner_rel, inner_bi->null_index))
+		is_dummy_partition(inner_rel, inner_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,
+							  outer_null_index, inner_null_index,
 							  jointype, &next_index, &null_index);
 	else
 		Assert(null_index == -1);
@@ -1478,6 +1573,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 +1584,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 +1893,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,
@@ -2527,19 +2625,41 @@ 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);
 	int			pos;
 	ListCell   *lc;
+	int			natts = 1;  //TODO: Handle for multi-column case
+	bool	   *null = NULL;
 
 	merged_bounds = (PartitionBoundInfo) palloc(sizeof(PartitionBoundInfoData));
 	merged_bounds->strategy = strategy;
-	merged_bounds->ndatums = ndatums;
 
+	if (merged_isnulls)
+	{
+		if (null_index >= 0)
+		{
+			null = (bool *) palloc0(sizeof(bool) * natts);
+			null[0] = true;
+			ndatums++;
+		}
+		merged_bounds->isnulls = (bool **) palloc(sizeof(bool *) * ndatums);
+
+		pos = 0;
+		foreach(lc, merged_isnulls)
+			merged_bounds->isnulls[pos++] = (bool *) lfirst(lc);
+
+		if (null_index >= 0)
+			merged_bounds->isnulls[pos] = null;
+	}
+
+	merged_bounds->ndatums = ndatums;
 	merged_bounds->datums = (Datum **) palloc(sizeof(Datum *) * ndatums);
+
 	pos = 0;
 	foreach(lc, merged_datums)
 		merged_bounds->datums[pos++] = (Datum *) lfirst(lc);
@@ -2556,6 +2676,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
 	{
@@ -2564,14 +2685,17 @@ build_merged_partition_bounds(char strategy, List *merged_datums,
 		merged_bounds->kind = 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;
+	if (merged_isnulls && null_index >= 0)
+		merged_bounds->indexes[pos] = null_index;
+
 	merged_bounds->default_index = default_index;
 
 	return merged_bounds;
@@ -3071,32 +3195,36 @@ 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 = 0;
+						int         offset = -1;
+						bool        equal = false;
+						List	   *elem = lfirst(cell);
+						Datum	   *values = (Datum *) palloc0(key->partnatts * sizeof(Datum));
+						bool	   *isnulls = (bool *) palloc0(key->partnatts * sizeof(bool));
+
+						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;
 						}
+
+						pfree(values);
+						pfree(isnulls);
 					}
 				}
 
@@ -3609,6 +3737,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
@@ -3618,8 +3788,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,
@@ -3632,10 +3802,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;
@@ -3805,13 +3975,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 = (*(PartitionListValue *const *) a)->values;
+	Datum	   *vals2 = (*(PartitionListValue *const *) b)->values;
+	bool	   *isnull1 = (*(PartitionListValue *const *) a)->isnulls;
+	bool	   *isnull2 = (*(PartitionListValue *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);
 }
 
 /*
@@ -3911,9 +4083,8 @@ make_partition_op_expr(PartitionKey key, int keynum,
 				int			nelems = list_length(elems);
 
 				Assert(nelems >= 1);
-				Assert(keynum == 0);
 
-				if (nelems > 1 &&
+				if (key->partnatts == 1 && nelems > 1 &&
 					!type_is_array(key->parttypid[keynum]))
 				{
 					ArrayExpr  *arrexpr;
@@ -3942,7 +4113,7 @@ make_partition_op_expr(PartitionKey key, int keynum,
 
 					result = (Expr *) saopexpr;
 				}
-				else
+				else if (key->partnatts == 1)
 				{
 					List	   *elemops = NIL;
 					ListCell   *lc;
@@ -3963,6 +4134,15 @@ make_partition_op_expr(PartitionKey key, int keynum,
 
 					result = nelems > 1 ? makeBoolExpr(OR_EXPR, elemops, -1) : linitial(elemops);
 				}
+				else
+				{
+					result = make_opclause(operoid,
+										   BOOLOID,
+										   false,
+										   arg1, arg2,
+										   InvalidOid,
+										   key->partcollation[keynum]);
+				}
 				break;
 			}
 
@@ -4087,11 +4267,8 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 	List	   *elems = NIL;
 	bool		list_has_null = false;
 
-	/*
-	 * Only single-column list partitioning is supported, so we are worried
-	 * only about the partition key with index 0.
-	 */
-	Assert(key->partnatts == 1);
+	if (key->partnatts > 1)
+		return get_qual_for_multi_column_list(parent, spec);
 
 	/* Construct Var or expression representing the partition column */
 	if (key->partattrs[0] != 0)
@@ -4117,13 +4294,8 @@ 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.
@@ -4135,6 +4307,12 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 		{
 			Const	   *val;
 
+			if (boundinfo->isnulls[i][0])
+			{
+				list_has_null = true;
+				continue;
+			}
+
 			/*
 			 * Construct Const from known-not-null datum.  We must be careful
 			 * to copy the value, because our result has to be able to outlive
@@ -4144,7 +4322,7 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 							key->parttypmod[0],
 							key->parttypcoll[0],
 							key->parttyplen[0],
-							datumCopy(*boundinfo->datums[i],
+							datumCopy(boundinfo->datums[i][0],
 									  key->parttypbyval[0],
 									  key->parttyplen[0]),
 							false,	/* isnull */
@@ -4160,12 +4338,17 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 		 */
 		foreach(cell, spec->listdatums)
 		{
-			Const	   *val = lfirst_node(Const, cell);
+			ListCell	   *cell2 = NULL;
 
-			if (val->constisnull)
-				list_has_null = true;
-			else
-				elems = lappend(elems, copyObject(val));
+			foreach(cell2, (List *) lfirst(cell))
+			{
+				Const      *val = castNode(Const, lfirst(cell2));
+
+				if (val->constisnull)
+					list_has_null = true;
+				else
+					elems = lappend(elems, copyObject(val));
+			}
 		}
 	}
 
@@ -4241,6 +4424,182 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 }
 
 /*
+ * get_qual_for_list_for_multi_column
+ *
+ * Returns a list of expressions to use as a list partition's constraint,
+ * given the parent relation and partition bound structure.
+ *
+ * Returns NIL for a default partition when it's the only partition since
+ * in that case there is no constraint.
+ */
+static List *
+get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
+{
+	int			i = 0;
+	int			j = 0;
+	PartitionKey key = RelationGetPartitionKey(parent);
+	List	   *result;
+	Expr	   *opexpr;
+	NullTest   *nulltest;
+	ListCell   *cell;
+	List	   *elems = NIL;
+	Expr      **keyCol = (Expr **) palloc0 (key->partnatts * sizeof(Expr *));
+
+	/* Construct Var or expression representing the partition columns */
+	for (i = 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;
+		}
+	}
+
+	/*
+	 * For default list partition, collect datums for all the partitions. The
+	 * default partition constraint should check that the partition key is
+	 * equal to none of those.
+	 */
+	if (spec->is_default)
+	{
+		int			ndatums = 0;
+		PartitionDesc pdesc = RelationGetPartitionDesc(parent, false);
+		PartitionBoundInfo boundinfo = pdesc->boundinfo;
+
+		if (boundinfo)
+			ndatums = boundinfo->ndatums;
+
+		/*
+		 * If default is the only partition, there need not be any partition
+		 * constraint on it.
+		 */
+		if (ndatums == 0)
+			return NIL;
+
+		for (i = 0; i < ndatums; i++)
+		{
+			List       *andexpr = NIL;
+
+			for (j = 0; j < key->partnatts; j++)
+			{
+				Const      *val = NULL;
+
+				if (boundinfo->isnulls[i][j])
+				{
+					nulltest = makeNode(NullTest);
+					nulltest->arg = keyCol[j];
+					nulltest->nulltesttype = IS_NULL;
+					nulltest->argisrow = false;
+					nulltest->location = -1;
+					andexpr = lappend(andexpr, nulltest);
+				}
+				else
+				{
+					/*
+					 * Gin up a "col IS NOT NULL" test that will be ANDed with
+					 * the each column's expression. This might seem redundant,
+					 * but the partition routing machinery needs it.
+					 */
+					nulltest = makeNode(NullTest);
+					nulltest->arg = keyCol[j];
+					nulltest->nulltesttype = IS_NOT_NULL;
+					nulltest->argisrow = false;
+					nulltest->location = -1;
+					andexpr = lappend(andexpr, nulltest);
+
+					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]);
+
+					opexpr = make_partition_op_expr(key, j, BTEqualStrategyNumber,
+													keyCol[j], (Expr *) val);
+					andexpr = lappend(andexpr, opexpr);
+				}
+			}
+
+			opexpr = makeBoolExpr(AND_EXPR, andexpr, -1);
+			elems = lappend(elems, opexpr);
+		}
+	}
+	else
+	{
+		/*
+		 * Create list of Consts for the allowed values.
+		 */
+		foreach(cell, spec->listdatums)
+		{
+			List	   *andexpr = NIL;
+			ListCell   *cell2 = NULL;
+
+			j = 0;
+			foreach(cell2, (List *) lfirst(cell))
+			{
+				Const      *val = castNode(Const, lfirst(cell2));
+
+				if (val->constisnull)
+				{
+					nulltest = makeNode(NullTest);
+					nulltest->arg = keyCol[j];
+					nulltest->nulltesttype = IS_NULL;
+					nulltest->argisrow = false;
+					nulltest->location = -1;
+					andexpr = lappend(andexpr, nulltest);
+				}
+				else
+				{
+					/*
+					 * Gin up a "col IS NOT NULL" test that will be ANDed with
+					 * the each column's expression. This might seem redundant,
+					 * but the partition routing machinery needs it.
+					 */
+					nulltest = makeNode(NullTest);
+					nulltest->arg = keyCol[j];
+					nulltest->nulltesttype = IS_NOT_NULL;
+					nulltest->argisrow = false;
+					nulltest->location = -1;
+					andexpr = lappend(andexpr, nulltest);
+
+					opexpr = make_partition_op_expr(key, j, BTEqualStrategyNumber,
+													keyCol[j], (Expr *) val);
+					andexpr = lappend(andexpr, opexpr);
+				}
+				j++;
+			}
+
+			opexpr = makeBoolExpr(AND_EXPR, andexpr, -1);
+			elems = lappend(elems, opexpr);
+		}
+	}
+
+	opexpr = makeBoolExpr(OR_EXPR, elems, -1);
+	result = list_make1(opexpr);
+
+	/*
+	 * Note that, in general, applying NOT to a constraint expression doesn't
+	 * necessarily invert the set of rows it accepts, because NOT (NULL) is
+	 * NULL.  However, the partition constraints we construct here never
+	 * evaluate to NULL, so applying NOT works as intended.
+	 */
+	if (spec->is_default)
+		result = list_make1(makeBoolExpr(NOT_EXPR, result, -1));
+
+	return result;
+}
+
+/*
  * get_qual_for_range
  *
  * Returns an implicit-AND list of expressions to use as a range partition's
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index e00edbe..76ea26c 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;
 
 /*
@@ -185,8 +187,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);
@@ -908,7 +910,8 @@ get_matching_partitions(PartitionPruneContext *context, List *pruning_steps)
 	{
 		Assert(context->strategy == PARTITION_STRATEGY_LIST);
 		Assert(partition_bound_accepts_nulls(context->boundinfo));
-		result = bms_add_member(result, context->boundinfo->null_index);
+		result = bms_add_member(result,
+								get_partition_bound_null_index(context->boundinfo));
 	}
 	if (scan_default)
 	{
@@ -1229,14 +1232,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 +1246,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 +1396,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 +1421,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 +2331,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;
 
-		return PARTCLAUSE_MATCH_NULLNESS;
+			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;
+			}
+
+			*pc = partclause;
+			return PARTCLAUSE_MATCH_CLAUSE;
+		}
 	}
 
 	/*
@@ -2637,6 +2671,169 @@ get_matching_hash_bounds(PartitionPruneContext *context,
 }
 
 /*
+ * 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;
+
+	/*
+	 * 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.
+	 */
+	if (off >= 0)
+	{
+		if (is_equal && nvalues < partnatts)
+		{
+			while (off >= 1 && off < boundinfo->ndatums - 1)
+			{
+				int32       cmpval;
+				int         nextoff;
+
+				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 = 0;
+	int j = 0;
+
+	Assert(minoff >= 0 && maxoff >= 0 && ncols > 0);
+	for (i = minoff; i < maxoff; i++)
+	{
+		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);
+	}
+}
+
+/*
  * get_matching_list_bounds
  *		Determine the offsets of list bounds matching the specified value,
  *		according to the semantics of the given operator strategy
@@ -2658,8 +2855,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,26 +2866,12 @@ 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,
 	 * just return the default partition if one exists.
@@ -2700,7 +2883,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,32 +2892,49 @@ 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;
 	}
 
 	/* Special case handling of values coming from a <> operator clause. */
 	if (opstrategy == InvalidStrategy)
 	{
+		int i = 0;
+
 		/*
 		 * 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
+			{
+				/*
+				 * 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);
 
-			/* We have a match. Remove from the result. */
-			Assert(boundinfo->indexes[off] >= 0);
-			result->bound_offsets = bms_del_member(result->bound_offsets,
-												   off);
+				/* 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 +2957,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 +3021,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 +3050,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;
 }
 
@@ -3343,6 +3559,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 +3581,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,10 +3608,11 @@ 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;
 
@@ -3405,26 +3625,35 @@ perform_pruning_base_step(PartitionPruneContext *context,
 			}
 
 			/* 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 +3680,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);
 
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 4df8cc5..75e431d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9407,10 +9407,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 = ", ";
 						}
 
@@ -11971,6 +11970,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 = NULL;
+	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 9db546d..0bb8518 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,6 +80,7 @@ typedef struct PartitionBoundInfoData
 	char		strategy;		/* hash, list or range? */
 	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 */
@@ -87,15 +89,15 @@ 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_partition_bound_null_index(PartitionBoundInfo boundinfo);
+
 extern int	get_hash_partition_greatest_modulus(PartitionBoundInfo b);
 extern uint64 compute_partition_hash_value(int partnatts, FmgrInfo *partsupfunc,
 										   Oid *partcollation,
@@ -130,15 +132,19 @@ 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,
 										  int nvalues, Datum *values, bool *is_equal);
 extern int	partition_hash_bsearch(PartitionBoundInfo boundinfo,
 								   int modulus, int remainder);
-
 #endif							/* PARTBOUNDS_H */
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..5e7187f 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,
@@ -919,6 +913,34 @@ 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;
+-- 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..158c1d9 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 (a = 'a'::text) AND (b IS NOT NULL) 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 (a = 'a'::text) AND (b IS NOT NULL) AND (b = 2)) OR ((a IS NOT NULL) AND (a = 'b'::text) AND (b IS NOT NULL) AND (b = 1)) OR ((a IS NOT NULL) AND (a = 'c'::text) AND (b IS NOT NULL) AND (b = 3)) OR ((a IS NOT NULL) AND (a = 'd'::text) AND (b IS NOT NULL) AND (b = 3)) OR ((a IS NOT NULL) AND (a = 'e'::text) AND (b IS NOT NULL) 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 IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL) AND (b = 3)) OR ((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL) AND (b = 4)) OR ((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NULL)) OR ((a IS NULL) AND (b IS NOT 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 IS NOT NULL) AND (a = 'b'::text) AND (b IS NULL)) OR ((a IS NULL) AND (b IS NOT 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_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..61e1129 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,
@@ -728,6 +722,25 @@ 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;
+
+-- 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_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

#18Amit Langote
amitlangote09@gmail.com
In reply to: Nitin Jadhav (#17)
Re: Multi-Column List Partitioning

Hi Nitin,

On Tue, Aug 31, 2021 at 8:02 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

The attached patch also fixes the above comments.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b, c);
create table p01 partition of p0 for values in ((1, 1, true), (NULL, 1, false));
create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

I guess that may be due to the following newly added code being incomplete:

+/*
+ * get_partition_bound_null_index
+ *
+ * Returns the partition index of the partition bound which accepts NULL.
+ */
+int
+get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+{
+   int i = 0;
+   int j = 0;
+
+   if (!boundinfo->isnulls)
+       return -1;
-           if (!val->constisnull)
-               count++;
+   for (i = 0; i < boundinfo->ndatums; i++)
+   {
+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)
+       {
+           if (boundinfo->isnulls[i][j])
+               return boundinfo->indexes[i];
        }
    }

+ return -1;
+}

Maybe this function needs to return a "bitmapset" of indexes, because
multiple partitions can now contain NULL values.

Some other issues I noticed and suggestions for improvement:

+/*
+ * checkForDuplicates
+ *
+ * Returns TRUE if the list bound element is already present in the list of
+ * list bounds, FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)

This function name may be too generic. Given that it is specific to
implementing list bound de-duplication, maybe the following signature
is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Also, better if the function comment mentions those parameter names, like:

"Returns TRUE if the list bound element 'new_bound' is already present
in the target list 'list_bounds', FALSE otherwise."

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw grammar
+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

+           colname[i] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd statement
overwrites its pointer by the pointer to the string palloc'd by
get_attname().

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate, exprLocation((Node
*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how about
writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
 typedef struct PartitionListValue
 {
    int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
 } PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Will send more comments in a bit...

--
Amit Langote
EDB: http://www.enterprisedb.com

#19Amit Langote
amitlangote09@gmail.com
In reply to: Amit Langote (#18)
1 attachment(s)
Re: Multi-Column List Partitioning

On Wed, Sep 1, 2021 at 2:31 PM Amit Langote <amitlangote09@gmail.com> wrote:

On Tue, Aug 31, 2021 at 8:02 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

The attached patch also fixes the above comments.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b, c);
create table p01 partition of p0 for values in ((1, 1, true), (NULL, 1, false));
create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

I guess that may be due to the following newly added code being incomplete:

+/*
+ * get_partition_bound_null_index
+ *
+ * Returns the partition index of the partition bound which accepts NULL.
+ */
+int
+get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+{
+   int i = 0;
+   int j = 0;
+
+   if (!boundinfo->isnulls)
+       return -1;
-           if (!val->constisnull)
-               count++;
+   for (i = 0; i < boundinfo->ndatums; i++)
+   {
+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)
+       {
+           if (boundinfo->isnulls[i][j])
+               return boundinfo->indexes[i];
}
}

+ return -1;
+}

Maybe this function needs to return a "bitmapset" of indexes, because
multiple partitions can now contain NULL values.

Some other issues I noticed and suggestions for improvement:

+/*
+ * checkForDuplicates
+ *
+ * Returns TRUE if the list bound element is already present in the list of
+ * list bounds, FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)

This function name may be too generic. Given that it is specific to
implementing list bound de-duplication, maybe the following signature
is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Also, better if the function comment mentions those parameter names, like:

"Returns TRUE if the list bound element 'new_bound' is already present
in the target list 'list_bounds', FALSE otherwise."

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw grammar
+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

+           colname[i] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd statement
overwrites its pointer by the pointer to the string palloc'd by
get_attname().

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate, exprLocation((Node
*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how about
writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Will send more comments in a bit...

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
  */

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+           all_values[j]->values = (Datum *) palloc0(key->partnatts *
sizeof(Datum));
+           all_values[j]->isnulls = (bool *) palloc0(key->partnatts *
sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
              qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts * sizeof(Datum));

Missing a newline between these two statements.

BTW, I noticed that the boundDatums variable is no longer used in
create_list_bounds. I traced back its origin and found that a recent
commit 53d86957e98 introduced it to implement an idea to reduce the
finer-grained pallocs that were being done in create_list_bounds(). I
don't think that this patch needs to throw away that work. You can
make it work as the attached delta patch that applies on top of v3.
Please check.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in partition_bounds_equal()
needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously mentioned.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the above
code stanza which appears to implement a short-cut for this long-form
logic.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

(Skipped looking at merge_list_bounds() and related changes for now as
I see a lot of TODOs remain to be done.)

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the logic
for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the same
goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits the
IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a few
other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

I still have more than half of v3 left to look at, so will continue
looking. In the meantime, please check the changes I suggested,
including the delta patch, and let me know your thoughts.

--
Amit Langote
EDB: http://www.enterprisedb.com

Attachments:

v3_delta_amit.patchapplication/octet-stream; name=v3_delta_amit.patchDownload
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index f4d3087bba..042d003b89 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -233,7 +233,6 @@ static Oid	get_partition_operator(PartitionKey key, int col,
 								   StrategyNumber strategy, bool *need_relabel);
 static List *get_qual_for_hash(Relation parent, PartitionBoundSpec *spec);
 static List *get_qual_for_list(Relation parent, PartitionBoundSpec *spec);
-static List *get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec);
 static List *get_qual_for_range(Relation parent, PartitionBoundSpec *spec,
 								bool for_default);
 static void get_range_key_properties(PartitionKey key, int keynum,
@@ -369,6 +368,7 @@ 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->isnulls = NULL;
 	boundinfo->default_index = -1;
@@ -457,8 +457,7 @@ partition_bound_accepts_nulls(PartitionBoundInfo boundinfo)
 
 	for (i = 0; i < boundinfo->ndatums; i++)
 	{
-		//TODO: Handle for multi-column cases
-		for (j = 0; j < 1; j++)
+		for (j = 0; j < boundinfo->partnatts; j++)
 		{
 			if (boundinfo->isnulls[i][j])
 				return true;
@@ -527,10 +526,12 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	int			next_index = 0;
 	int			default_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->default_index = -1;
 
@@ -604,7 +605,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 -
@@ -616,17 +618,15 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	{
 		int         j = 0;
 		int			orig_index = all_values[i]->index;
-		boundinfo->datums[i] = (Datum *) palloc(key->partnatts * sizeof(Datum));
-		boundinfo->isnulls[i] = (bool *) palloc(key->partnatts * sizeof(bool));
-
 
+		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];
 		}
 
@@ -734,6 +734,7 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	boundinfo = (PartitionBoundInfoData *)
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
+	boundinfo->partnatts = key->partnatts;
 	boundinfo->isnulls = NULL;
 	/* Will be set correctly below. */
 	boundinfo->default_index = -1;
@@ -4079,14 +4080,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);
-
-				if (key->partnatts == 1 && 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;
 
@@ -4113,8 +4110,9 @@ make_partition_op_expr(PartitionKey key, int keynum,
 
 					result = (Expr *) saopexpr;
 				}
-				else if (key->partnatts == 1)
+				else if (IsA(arg2, List) && list_length((List *) arg2) > 1)
 				{
+					List	   *elems = (List *) arg2;
 					List	   *elemops = NIL;
 					ListCell   *lc;
 
@@ -4132,14 +4130,16 @@ 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, arg2,
+										   arg1,
+										   IsA(arg2, List) ?
+										   linitial((List *) arg2) : arg2,
 										   InvalidOid,
 										   key->partcollation[keynum]);
 				}
@@ -4259,207 +4259,39 @@ static List *
 get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 {
 	PartitionKey key = RelationGetPartitionKey(parent);
-	List	   *result;
-	Expr	   *keyCol;
-	Expr	   *opexpr;
-	NullTest   *nulltest;
-	ListCell   *cell;
-	List	   *elems = NIL;
-	bool		list_has_null = false;
-
-	if (key->partnatts > 1)
-		return get_qual_for_multi_column_list(parent, spec);
-
-	/* 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));
-
-	/*
-	 * For default list partition, collect datums for all the partitions. The
-	 * default partition constraint should check that the partition key is
-	 * equal to none of those.
-	 */
-	if (spec->is_default)
-	{
-		int			i;
-		int			ndatums = 0;
-		PartitionDesc pdesc = RelationGetPartitionDesc(parent, false);
-		PartitionBoundInfo boundinfo = pdesc->boundinfo;
-
-		if (boundinfo)
-			ndatums = boundinfo->ndatums;
-
-		/*
-		 * If default is the only partition, there need not be any partition
-		 * constraint on it.
-		 */
-		if (ndatums == 0 && !list_has_null)
-			return NIL;
-
-		for (i = 0; i < ndatums; i++)
-		{
-			Const	   *val;
-
-			if (boundinfo->isnulls[i][0])
-			{
-				list_has_null = true;
-				continue;
-			}
-
-			/*
-			 * 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.
-			 */
-			val = makeConst(key->parttypid[0],
-							key->parttypmod[0],
-							key->parttypcoll[0],
-							key->parttyplen[0],
-							datumCopy(boundinfo->datums[i][0],
-									  key->parttypbyval[0],
-									  key->parttyplen[0]),
-							false,	/* isnull */
-							key->parttypbyval[0]);
-
-			elems = lappend(elems, val);
-		}
-	}
-	else
-	{
-		/*
-		 * Create list of Consts for the allowed values, excluding any nulls.
-		 */
-		foreach(cell, spec->listdatums)
-		{
-			ListCell	   *cell2 = NULL;
-
-			foreach(cell2, (List *) lfirst(cell))
-			{
-				Const      *val = castNode(Const, lfirst(cell2));
-
-				if (val->constisnull)
-					list_has_null = true;
-				else
-					elems = lappend(elems, copyObject(val));
-			}
-		}
-	}
-
-	if (elems)
-	{
-		/*
-		 * Generate the operator expression from the non-null partition
-		 * values.
-		 */
-		opexpr = make_partition_op_expr(key, 0, BTEqualStrategyNumber,
-										keyCol, (Expr *) elems);
-	}
-	else
-	{
-		/*
-		 * If there are no partition values, we don't need an operator
-		 * expression.
-		 */
-		opexpr = NULL;
-	}
-
-	if (!list_has_null)
-	{
-		/*
-		 * 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);
-	}
-	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;
-
-		if (opexpr)
-		{
-			Expr	   *or;
-
-			or = makeBoolExpr(OR_EXPR, list_make2(nulltest, opexpr), -1);
-			result = list_make1(or);
-		}
-		else
-			result = list_make1(nulltest);
-	}
-
-	/*
-	 * Note that, in general, applying NOT to a constraint expression doesn't
-	 * necessarily invert the set of rows it accepts, because NOT (NULL) is
-	 * NULL.  However, the partition constraints we construct here never
-	 * evaluate to NULL, so applying NOT works as intended.
-	 */
-	if (spec->is_default)
-	{
-		result = list_make1(make_ands_explicit(result));
-		result = list_make1(makeBoolExpr(NOT_EXPR, result, -1));
-	}
-
-	return result;
-}
-
-/*
- * get_qual_for_list_for_multi_column
- *
- * Returns a list of expressions to use as a list partition's constraint,
- * given the parent relation and partition bound structure.
- *
- * Returns NIL for a default partition when it's the only partition since
- * in that case there is no constraint.
- */
-static List *
-get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
-{
-	int			i = 0;
-	int			j = 0;
-	PartitionKey key = RelationGetPartitionKey(parent);
-	List	   *result;
-	Expr	   *opexpr;
-	NullTest   *nulltest;
+	List	   *result = NIL;
+	Expr	   *datumtest;
+	Expr	   *is_null_test = NULL;
+	List	   *datum_elems = NIL;
 	ListCell   *cell;
-	List	   *elems = NIL;
+	bool		key_is_null[PARTITION_MAX_KEYS];
+	int			i,
+				j;
 	Expr      **keyCol = (Expr **) palloc0 (key->partnatts * sizeof(Expr *));
 
-	/* Construct Var or expression representing the partition columns */
-	for (i = 0; i < key->partnatts; i++)
+	/* 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);
+										 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;
 	}
 
 	/*
@@ -4469,6 +4301,7 @@ get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
 	 */
 	if (spec->is_default)
 	{
+		int			i;
 		int			ndatums = 0;
 		PartitionDesc pdesc = RelationGetPartitionDesc(parent, false);
 		PartitionBoundInfo boundinfo = pdesc->boundinfo;
@@ -4480,40 +4313,42 @@ get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
 		 * If default is the only partition, there need not be any partition
 		 * constraint on it.
 		 */
-		if (ndatums == 0)
+		if (ndatums == 0 && !partition_bound_accepts_nulls(boundinfo))
 			return NIL;
 
 		for (i = 0; i < ndatums; i++)
 		{
-			List       *andexpr = NIL;
+			List	   *and_args = NIL;
+			Expr	   *datum_elem = NULL;
 
+			/*
+			 * For the multi-column case, we must make an BoolExpr that
+			 * ANDs the results of the expressions for various columns,
+			 * where each expresion is either an IS NULL test or an
+			 * OpExpr comparing the column against a non-NULL datum.
+			 */
 			for (j = 0; j < key->partnatts; j++)
 			{
 				Const      *val = NULL;
 
 				if (boundinfo->isnulls[i][j])
 				{
-					nulltest = makeNode(NullTest);
+					NullTest   *nulltest = makeNode(NullTest);
+
+					key_is_null[j] = true;
+
 					nulltest->arg = keyCol[j];
 					nulltest->nulltesttype = IS_NULL;
 					nulltest->argisrow = false;
 					nulltest->location = -1;
-					andexpr = lappend(andexpr, nulltest);
+
+					if (key->partnatts > 1)
+						and_args = lappend(and_args, nulltest);
+					else
+						is_null_test = (Expr *) nulltest;
 				}
 				else
 				{
-					/*
-					 * Gin up a "col IS NOT NULL" test that will be ANDed with
-					 * the each column's expression. This might seem redundant,
-					 * but the partition routing machinery needs it.
-					 */
-					nulltest = makeNode(NullTest);
-					nulltest->arg = keyCol[j];
-					nulltest->nulltesttype = IS_NOT_NULL;
-					nulltest->argisrow = false;
-					nulltest->location = -1;
-					andexpr = lappend(andexpr, nulltest);
-
 					val = makeConst(key->parttypid[j],
 									key->parttypmod[j],
 									key->parttypcoll[j],
@@ -4524,68 +4359,143 @@ get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
 									false,  /* isnull */
 									key->parttypbyval[j]);
 
-					opexpr = make_partition_op_expr(key, j, BTEqualStrategyNumber,
-													keyCol[j], (Expr *) val);
-					andexpr = lappend(andexpr, opexpr);
+					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;
 				}
 			}
 
-			opexpr = makeBoolExpr(AND_EXPR, andexpr, -1);
-			elems = lappend(elems, opexpr);
+			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.
-		 */
 		foreach(cell, spec->listdatums)
 		{
-			List	   *andexpr = NIL;
-			ListCell   *cell2 = NULL;
+			List	   *listbound = (List *) lfirst(cell);
+			ListCell   *cell2;
+			List	   *and_args = NIL;
+			Expr	   *datum_elem = NULL;
 
+			/*
+			 * See the comment above regarding the handling for the
+			 * multi-column case.
+			 */
 			j = 0;
-			foreach(cell2, (List *) lfirst(cell))
+			foreach(cell2, listbound)
 			{
 				Const      *val = castNode(Const, lfirst(cell2));
 
 				if (val->constisnull)
 				{
-					nulltest = makeNode(NullTest);
+					NullTest   *nulltest = makeNode(NullTest);
+
+					key_is_null[j] = true;
+
 					nulltest->arg = keyCol[j];
 					nulltest->nulltesttype = IS_NULL;
 					nulltest->argisrow = false;
 					nulltest->location = -1;
-					andexpr = lappend(andexpr, nulltest);
+
+					if (key->partnatts > 1)
+						and_args = lappend(and_args, nulltest);
+					else
+						is_null_test = (Expr *) nulltest;
 				}
 				else
 				{
-					/*
-					 * Gin up a "col IS NOT NULL" test that will be ANDed with
-					 * the each column's expression. This might seem redundant,
-					 * but the partition routing machinery needs it.
-					 */
-					nulltest = makeNode(NullTest);
-					nulltest->arg = keyCol[j];
-					nulltest->nulltesttype = IS_NOT_NULL;
-					nulltest->argisrow = false;
-					nulltest->location = -1;
-					andexpr = lappend(andexpr, nulltest);
-
-					opexpr = make_partition_op_expr(key, j, BTEqualStrategyNumber,
-													keyCol[j], (Expr *) val);
-					andexpr = lappend(andexpr, opexpr);
+					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++;
 			}
 
-			opexpr = makeBoolExpr(AND_EXPR, andexpr, -1);
-			elems = lappend(elems, opexpr);
+			if (list_length(and_args) > 1)
+				datum_elem = makeBoolExpr(AND_EXPR, and_args, -1);
+
+			if (datum_elem)
+				datum_elems = lappend(datum_elems, datum_elem);
 		}
 	}
 
-	opexpr = makeBoolExpr(OR_EXPR, elems, -1);
-	result = list_make1(opexpr);
+	/*
+	 * 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 (!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);
+		}
+	}
+
+	/*
+	 * 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)
+	{
+		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
+		datumtest = NULL;
+
+	/*
+	 * 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);
+
+		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
@@ -4594,7 +4504,10 @@ get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
 	 * evaluate to NULL, so applying NOT works as intended.
 	 */
 	if (spec->is_default)
+	{
+		result = list_make1(make_ands_explicit(result));
 		result = list_make1(makeBoolExpr(NOT_EXPR, result, -1));
+	}
 
 	return result;
 }
diff --git a/src/include/partitioning/partbounds.h b/src/include/partitioning/partbounds.h
index 0bb851801e..35991a0227 100644
--- a/src/include/partitioning/partbounds.h
+++ b/src/include/partitioning/partbounds.h
@@ -78,6 +78,7 @@ 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;
diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out
index 158c1d9d63..038cc5395e 100644
--- a/src/test/regress/expected/insert.out
+++ b/src/test/regress/expected/insert.out
@@ -1067,7 +1067,7 @@ Partitions: mclparted_p1 FOR VALUES IN (('a', 1)),
  a      | text    |           |          |         | extended |              | 
  b      | integer |           |          |         | plain    |              | 
 Partition of: mclparted FOR VALUES IN (('a', 1))
-Partition constraint: (((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL) AND (b = 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"
@@ -1076,7 +1076,7 @@ Partition constraint: (((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL)
  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 (a = 'a'::text) AND (b IS NOT NULL) AND (b = 2)) OR ((a IS NOT NULL) AND (a = 'b'::text) AND (b IS NOT NULL) AND (b = 1)) OR ((a IS NOT NULL) AND (a = 'c'::text) AND (b IS NOT NULL) AND (b = 3)) OR ((a IS NOT NULL) AND (a = 'd'::text) AND (b IS NOT NULL) AND (b = 3)) OR ((a IS NOT NULL) AND (a = 'e'::text) AND (b IS NOT NULL) AND (b = 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"
@@ -1085,7 +1085,7 @@ Partition constraint: (((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL)
  a      | text    |           |          |         | extended |              | 
  b      | integer |           |          |         | plain    |              | 
 Partition of: mclparted FOR VALUES IN (('a', 3), ('a', 4), ('a', NULL), (NULL, 1))
-Partition constraint: (((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL) AND (b = 3)) OR ((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL) AND (b = 4)) OR ((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NULL)) OR ((a IS NULL) AND (b IS NOT NULL) AND (b = 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"
@@ -1094,7 +1094,7 @@ Partition constraint: (((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL)
  a      | text    |           |          |         | extended |              | 
  b      | integer |           |          |         | plain    |              | 
 Partition of: mclparted FOR VALUES IN (('b', NULL), (NULL, 2))
-Partition constraint: (((a IS NOT NULL) AND (a = 'b'::text) AND (b IS NULL)) OR ((a IS NULL) AND (b IS NOT NULL) AND (b = 2)))
+Partition constraint: (((a = 'b'::text) AND (b IS NULL)) OR ((a IS NULL) AND (b = 2)))
 
 \d+ mclparted_p5
                                 Table "public.mclparted_p5"
#20Rajkumar Raghuwanshi
rajkumar.raghuwanshi@enterprisedb.com
In reply to: Amit Langote (#19)
Re: Multi-Column List Partitioning

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is failing
with below errors.

--inherit.sql is failing with error :"ERROR: negative bitmapset member not
allowed"
update mlparted_tab mlp set c = 'xxx'
from
(select a from some_tab union all select a+1 from some_tab) ss (a)
where (mlp.a = ss.a and mlp.b = 'b') or mlp.a = 3;
ERROR: negative bitmapset member not allowed

--partition_join.sql is crashing with enable_partitionwise_join set to true.
CREATE TABLE plt1_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt1_adv_p1 PARTITION OF plt1_adv FOR VALUES IN ('0001',
'0003');
CREATE TABLE plt1_adv_p2 PARTITION OF plt1_adv FOR VALUES IN ('0004',
'0006');
CREATE TABLE plt1_adv_p3 PARTITION OF plt1_adv FOR VALUES IN ('0008',
'0009');
INSERT INTO plt1_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM
generate_series(1, 299) i WHERE i % 10 IN (1, 3, 4, 6, 8, 9);
ANALYZE plt1_adv;
CREATE TABLE plt2_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt2_adv_p1 PARTITION OF plt2_adv FOR VALUES IN ('0002',
'0003');
CREATE TABLE plt2_adv_p2 PARTITION OF plt2_adv FOR VALUES IN ('0004',
'0006');
CREATE TABLE plt2_adv_p3 PARTITION OF plt2_adv FOR VALUES IN ('0007',
'0009');
INSERT INTO plt2_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM
generate_series(1, 299) i WHERE i % 10 IN (2, 3, 4, 6, 7, 9);
ANALYZE plt2_adv;
-- inner join
EXPLAIN (COSTS OFF)
SELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON
(t1.a = t2.a AND t1.c = t2.c) WHERE t1.b < 10 ORDER BY t1.a;
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost

--stack-trace
Core was generated by `postgres: edb regression [local] EXPLAIN
'.
Program terminated with signal 6, Aborted.
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install
glibc-2.17-222.el7.x86_64 keyutils-libs-1.5.8-3.el7.x86_64
krb5-libs-1.15.1-19.el7.x86_64 libcom_err-1.42.9-12.el7_5.x86_64
libgcc-4.8.5-39.el7.x86_64 libselinux-2.5-12.el7.x86_64
openssl-libs-1.0.2k-19.el7.x86_64 pcre-8.32-17.el7.x86_64
zlib-1.2.7-17.el7.x86_64
(gdb) bt
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
#1 0x00007f7d339bb968 in abort () from /lib64/libc.so.6
#2 0x0000000000b0fbc3 in ExceptionalCondition (conditionName=0xcbda10
"part_index >= 0", errorType=0xcbd1c3 "FailedAssertion", fileName=0xcbd2fe
"partbounds.c", lineNumber=1957)
at assert.c:69
#3 0x0000000000892aa1 in is_dummy_partition (rel=0x19b37c0, part_index=-1)
at partbounds.c:1957
#4 0x00000000008919bd in merge_list_bounds (partnatts=1,
partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0,
inner_rel=0x1922938, jointype=JOIN_INNER,
outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at
partbounds.c:1529
#5 0x00000000008910de in partition_bounds_merge (partnatts=1,
partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0,
inner_rel=0x1922938, jointype=JOIN_INNER,
outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at
partbounds.c:1223
#6 0x000000000082c41a in compute_partition_bounds (root=0x1a19ed0,
rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
parent_sjinfo=0x7fffd67752a0, parts1=0x7fffd67751b0,
parts2=0x7fffd67751a8) at joinrels.c:1644
#7 0x000000000082bc34 in try_partitionwise_join (root=0x1a19ed0,
rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
parent_sjinfo=0x7fffd67752a0, parent_restrictlist=0x1ab3318)
at joinrels.c:1402
#8 0x000000000082aea2 in populate_joinrel_with_paths (root=0x1a19ed0,
rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30, sjinfo=0x7fffd67752a0,
restrictlist=0x1ab3318)
at joinrels.c:926
#9 0x000000000082a8f5 in make_join_rel (root=0x1a19ed0, rel1=0x19b37c0,
rel2=0x1922938) at joinrels.c:760
#10 0x0000000000829e03 in make_rels_by_clause_joins (root=0x1a19ed0,
old_rel=0x19b37c0, other_rels_list=0x1ab2970, other_rels=0x1ab2990) at
joinrels.c:312
#11 0x00000000008298d9 in join_search_one_level (root=0x1a19ed0, level=2)
at joinrels.c:123
#12 0x000000000080c566 in standard_join_search (root=0x1a19ed0,
levels_needed=2, initial_rels=0x1ab2970) at allpaths.c:3020
#13 0x000000000080c4df in make_rel_from_joinlist (root=0x1a19ed0,
joinlist=0x199d538) at allpaths.c:2951
#14 0x000000000080816b in make_one_rel (root=0x1a19ed0, joinlist=0x199d538)
at allpaths.c:228
#15 0x000000000084491d in query_planner (root=0x1a19ed0,
qp_callback=0x84a538 <standard_qp_callback>, qp_extra=0x7fffd6775630) at
planmain.c:276
#16 0x0000000000847040 in grouping_planner (root=0x1a19ed0,
tuple_fraction=0) at planner.c:1447
#17 0x0000000000846709 in subquery_planner (glob=0x19b39d8,
parse=0x1aaa290, parent_root=0x0, hasRecursion=false, tuple_fraction=0) at
planner.c:1025
#18 0x0000000000844f3e in standard_planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a,
t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c =
t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048, boundParams=0x0)
at planner.c:406
#19 0x0000000000844ce9 in planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a,
t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c =
t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048, boundParams=0x0)
at planner.c:277
#20 0x0000000000978483 in pg_plan_query (querytree=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a,
t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c =
t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048, boundParams=0x0)
at postgres.c:847
#21 0x00000000006937fc in ExplainOneQuery (query=0x1aaa290,
cursorOptions=2048, into=0x0, es=0x19b36f0,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a,
t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c =
t2.c) WHERE t1.b < 10 ORDER BY t1.a;",
params=0x0, queryEnv=0x0) at explain.c:397
#22 0x0000000000693351 in ExplainQuery (pstate=0x197c410, stmt=0x1aaa0b0,
params=0x0, dest=0x197c378) at explain.c:281
#23 0x00000000009811fa in standard_ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a,
t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c =
t2.c) WHERE t1.b < 10 ORDER BY t1.a;",
readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,
queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at utility.c:845
#24 0x00000000009809ec in ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a,
t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c =
t2.c) WHERE t1.b < 10 ORDER BY t1.a;",
readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,
queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at utility.c:527
#25 0x000000000097f636 in PortalRunUtility (portal=0x1893b40,
pstmt=0x1a0bfc8, isTopLevel=true, setHoldSnapshot=true, dest=0x197c378,
qc=0x7fffd6775f90) at pquery.c:1147
#26 0x000000000097f3a5 in FillPortalStore (portal=0x1893b40,
isTopLevel=true) at pquery.c:1026
#27 0x000000000097ed11 in PortalRun (portal=0x1893b40,
count=9223372036854775807, isTopLevel=true, run_once=true, dest=0x1a0c0b8,
altdest=0x1a0c0b8, qc=0x7fffd6776150) at pquery.c:758
#28 0x0000000000978aa5 in exec_simple_query (

Thanks & Regards,
Rajkumar Raghuwanshi

On Fri, Sep 3, 2021 at 7:17 PM Amit Langote <amitlangote09@gmail.com> wrote:

Show quoted text

On Wed, Sep 1, 2021 at 2:31 PM Amit Langote <amitlangote09@gmail.com>
wrote:

On Tue, Aug 31, 2021 at 8:02 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

The attached patch also fixes the above comments.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b, c);
create table p01 partition of p0 for values in ((1, 1, true), (NULL, 1,

false));

create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

I guess that may be due to the following newly added code being

incomplete:

+/*
+ * get_partition_bound_null_index
+ *
+ * Returns the partition index of the partition bound which accepts

NULL.

+ */
+int
+get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+{
+   int i = 0;
+   int j = 0;
+
+   if (!boundinfo->isnulls)
+       return -1;
-           if (!val->constisnull)
-               count++;
+   for (i = 0; i < boundinfo->ndatums; i++)
+   {
+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)
+       {
+           if (boundinfo->isnulls[i][j])
+               return boundinfo->indexes[i];
}
}

+ return -1;
+}

Maybe this function needs to return a "bitmapset" of indexes, because
multiple partitions can now contain NULL values.

Some other issues I noticed and suggestions for improvement:

+/*
+ * checkForDuplicates
+ *
+ * Returns TRUE if the list bound element is already present in the

list of

+ * list bounds, FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)

This function name may be too generic. Given that it is specific to
implementing list bound de-duplication, maybe the following signature
is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Also, better if the function comment mentions those parameter names,

like:

"Returns TRUE if the list bound element 'new_bound' is already present
in the target list 'list_bounds', FALSE otherwise."

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw

grammar

+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

+           colname[i] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd statement
overwrites its pointer by the pointer to the string palloc'd by
get_attname().

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate, exprLocation((Node
*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how about
writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Will send more comments in a bit...

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
*/

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+           all_values[j]->values = (Datum *) palloc0(key->partnatts *
sizeof(Datum));
+           all_values[j]->isnulls = (bool *) palloc0(key->partnatts *
sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts *
sizeof(Datum));

Missing a newline between these two statements.

BTW, I noticed that the boundDatums variable is no longer used in
create_list_bounds. I traced back its origin and found that a recent
commit 53d86957e98 introduced it to implement an idea to reduce the
finer-grained pallocs that were being done in create_list_bounds(). I
don't think that this patch needs to throw away that work. You can
make it work as the attached delta patch that applies on top of v3.
Please check.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in partition_bounds_equal()
needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously mentioned.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the above
code stanza which appears to implement a short-cut for this long-form
logic.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

(Skipped looking at merge_list_bounds() and related changes for now as
I see a lot of TODOs remain to be done.)

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the logic
for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the same
goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits the
IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a few
other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

I still have more than half of v3 left to look at, so will continue
looking. In the meantime, please check the changes I suggested,
including the delta patch, and let me know your thoughts.

--
Amit Langote
EDB: http://www.enterprisedb.com

#21Amit Langote
amitlangote09@gmail.com
In reply to: Rajkumar Raghuwanshi (#20)
1 attachment(s)
Re: Multi-Column List Partitioning

Hi,

On Mon, Sep 13, 2021 at 7:17 PM Rajkumar Raghuwanshi
<rajkumar.raghuwanshi@enterprisedb.com> wrote:

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is failing with below errors.

Thanks Rajkumar for testing.

--inherit.sql is failing with error :"ERROR: negative bitmapset member not allowed"
update mlparted_tab mlp set c = 'xxx'
from
(select a from some_tab union all select a+1 from some_tab) ss (a)
where (mlp.a = ss.a and mlp.b = 'b') or mlp.a = 3;
ERROR: negative bitmapset member not allowed

--partition_join.sql is crashing with enable_partitionwise_join set to true.

Here's a v2 of the delta patch that should fix both of these test
failures. As I mentioned in my last reply, my delta patch fixed what
I think were problems in Nitin's v3 patch but were not complete by
themselves. Especially, I hadn't bothered to investigate various /*
TODO: handle multi-column list partitioning */ sites to deal with my
own changes.

In the attached updated version, I've dealt with some of those such
that at least the existing cases exercising partition pruning and
partition wise joins now pass.

I thought about sending a v4 of the main patch with my proposed
changes so far integrated, but decided to just post a delta_v2 for
now.

--
Amit Langote
EDB: http://www.enterprisedb.com

Attachments:

v3_delta_amit_v2.patchapplication/octet-stream; name=v3_delta_amit_v2.patchDownload
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index a442b3e6cf..c56fe3e330 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -180,7 +180,6 @@ static PartitionBoundInfo build_merged_partition_bounds(char strategy,
 														List *merged_isnulls,
 														List *merged_kinds,
 														List *merged_indexes,
-														int null_index,
 														int default_index);
 static int	get_range_partition(RelOptInfo *rel,
 								PartitionBoundInfo bi,
@@ -233,7 +232,6 @@ static Oid	get_partition_operator(PartitionKey key, int col,
 								   StrategyNumber strategy, bool *need_relabel);
 static List *get_qual_for_hash(Relation parent, PartitionBoundSpec *spec);
 static List *get_qual_for_list(Relation parent, PartitionBoundSpec *spec);
-static List *get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec);
 static List *get_qual_for_range(Relation parent, PartitionBoundSpec *spec,
 								bool for_default);
 static void get_range_key_properties(PartitionKey key, int keynum,
@@ -369,6 +367,7 @@ 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->isnulls = NULL;
 	boundinfo->default_index = -1;
@@ -457,8 +456,7 @@ partition_bound_accepts_nulls(PartitionBoundInfo boundinfo)
 
 	for (i = 0; i < boundinfo->ndatums; i++)
 	{
-		//TODO: Handle for multi-column cases
-		for (j = 0; j < 1; j++)
+		for (j = 0; j < boundinfo->partnatts; j++)
 		{
 			if (boundinfo->isnulls[i][j])
 				return true;
@@ -469,30 +467,40 @@ partition_bound_accepts_nulls(PartitionBoundInfo boundinfo)
 }
 
 /*
- * get_partition_bound_null_index
+ * get_partition_bound_null_indexes
  *
- * Returns the partition index of the partition bound which accepts NULL.
+ * Returns the partition indexes of partitions which accept NULL in
+ * the specified key columns.
  */
-int
-get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+Bitmapset *
+get_partition_bound_null_indexes(PartitionBoundInfo boundinfo,
+								 Bitmapset *nullkeys)
 {
-	int i = 0;
-	int j = 0;
+	Bitmapset  *result = NULL;
+	int			i;
 
-	if (!boundinfo->isnulls)
-		return -1;
+	if (!boundinfo->isnulls || bms_is_empty(nullkeys))
+		return NULL;
 
 	for (i = 0; i < boundinfo->ndatums; i++)
 	{
-		//TODO: Handle for multi-column cases
-		for (j = 0; j < 1; j++)
+		int		j = -1;
+		bool	add_part = true;
+
+		while ((j = bms_next_member(nullkeys, j)) >= 0)
 		{
-			if (boundinfo->isnulls[i][j])
-				return boundinfo->indexes[i];
+			if (!boundinfo->isnulls[i][j])
+			{
+				add_part = false;
+				break;
+			}
 		}
+
+		if (add_part)
+			result = bms_add_member(result, boundinfo->indexes[i]);
 	}
 
-	return -1;
+	return result;
 }
 
 /*
@@ -527,10 +535,12 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	int			next_index = 0;
 	int			default_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->default_index = -1;
 
@@ -604,7 +614,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 -
@@ -616,17 +627,15 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	{
 		int         j = 0;
 		int			orig_index = all_values[i]->index;
-		boundinfo->datums[i] = (Datum *) palloc(key->partnatts * sizeof(Datum));
-		boundinfo->isnulls[i] = (bool *) palloc(key->partnatts * sizeof(bool));
-
 
+		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];
 		}
 
@@ -734,6 +743,7 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	boundinfo = (PartitionBoundInfoData *)
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
+	boundinfo->partnatts = key->partnatts;
 	boundinfo->isnulls = NULL;
 	/* Will be set correctly below. */
 	boundinfo->default_index = -1;
@@ -949,9 +959,6 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 	if (b1->nindexes != b2->nindexes)
 		return false;
 
-	if (get_partition_bound_null_index(b1) != get_partition_bound_null_index(b2))
-		return false;
-
 	if (b1->default_index != b2->default_index)
 		return false;
 
@@ -1277,16 +1284,11 @@ merge_list_bounds(int partnatts,
 	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);
-	int			outer_null_index = get_partition_bound_null_index(outer_bi);
-	int			inner_null_index = get_partition_bound_null_index(inner_bi);
 	PartitionMap outer_map;
 	PartitionMap inner_map;
 	int			outer_pos;
 	int			inner_pos;
 	int			next_index = 0;
-	int			null_index = -1;
 	int			default_index = -1;
 	List	   *merged_datums = NIL;
 	List	   *merged_indexes = NIL;
@@ -1328,34 +1330,12 @@ merge_list_bounds(int partnatts,
 		int			cmpval;
 		Datum	   *merged_datum = NULL;
 		int			merged_index = -1;
-		bool	   *outer_isnull;
-		bool	   *inner_isnull;
+		bool	   *outer_isnull = NULL;
+		bool	   *inner_isnull = NULL;
 		bool	   *merged_isnull = NULL;
-
-		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];
-
-		//TODO: Handle for multi-column case.
-		if (outer_isnull[0] && inner_isnull[0])
-		{
-			outer_pos++;
-			inner_pos++;
-			continue;
-		}
-		else if (outer_isnull[0])
-		{
-			outer_pos++;
-			continue;
-		}
-		else if (inner_isnull[0])
-		{
-			inner_pos++;
-			continue;
-		}
-
+		bool		outer_has_null = false;
+		bool		inner_has_null = false;
+		int			i;
 
 		if (outer_pos < outer_bi->ndatums)
 		{
@@ -1389,6 +1369,66 @@ merge_list_bounds(int partnatts,
 			outer_bi->datums[outer_pos] : NULL;
 		inner_datums = inner_pos < inner_bi->ndatums ?
 			inner_bi->datums[inner_pos] : NULL;
+		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_isnull)
+		{
+			for (i = 0; i < partnatts; i++)
+			{
+				if (outer_isnull[i])
+					outer_has_null = true;
+			}
+		}
+		if (inner_isnull)
+		{
+			for (i = 0; i < partnatts; i++)
+			{
+				if (inner_isnull[i])
+					inner_has_null = true;
+			}
+		}
+
+		if ((outer_index < outer_bi->ndatums && outer_has_null) ||
+			(inner_index < inner_bi->ndatums && inner_has_null))
+		{
+			merge_null_partitions(&outer_map, &inner_map,
+								  outer_has_null, inner_has_null,
+								  outer_index, inner_index,
+								  jointype,
+								  &next_index, &merged_index);
+
+			/*
+			 * If we assigned a merged partition, add the list bound and
+			 * index of the merged partition if appropriate.
+			 */
+			if (merged_index >= 0 && merged_index != default_index)
+				merged_indexes = lappend_int(merged_indexes, merged_index);
+
+			if (outer_has_null && inner_has_null)
+			{
+				merged_datums = lappend(merged_datums, outer_datums);
+				merged_isnulls = lappend(merged_isnulls, outer_isnull);
+				outer_pos++;
+				inner_pos++;
+			}
+			else if (outer_has_null)
+			{
+				merged_datums = lappend(merged_datums, outer_datums);
+				merged_isnulls = lappend(merged_isnulls, outer_isnull);
+				outer_pos++;
+			}
+			else
+			{
+				merged_datums = lappend(merged_datums, inner_datums);
+				merged_isnulls = lappend(merged_isnulls, inner_isnull);
+				inner_pos++;
+			}
+
+			continue;
+		}
 
 		/*
 		 * We run this loop till both sides finish.  This allows us to avoid
@@ -1406,7 +1446,6 @@ merge_list_bounds(int partnatts,
 		else
 		{
 			Assert(outer_datums != NULL && inner_datums != NULL);
-			//TODO: handle multi-column case
 			cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
 												outer_datums, outer_isnull,
 												inner_datums, inner_isnull,
@@ -1520,26 +1559,6 @@ merge_list_bounds(int partnatts,
 		}
 	}
 
-	/*
-	 * If the NULL partitions (if any) have been proven empty, deem them
-	 * non-existent.
-	 */
-	if (outer_has_null &&
-		is_dummy_partition(outer_rel, outer_null_index))
-		outer_has_null = false;
-	if (inner_has_null &&
-		is_dummy_partition(inner_rel, inner_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_null_index, inner_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,
@@ -1576,7 +1595,6 @@ merge_list_bounds(int partnatts,
 													  merged_isnulls,
 													  NIL,
 													  merged_indexes,
-													  null_index,
 													  default_index);
 		Assert(merged_bounds);
 	}
@@ -1896,7 +1914,6 @@ merge_range_bounds(int partnatts, FmgrInfo *partsupfuncs,
 													  NIL,
 													  merged_kinds,
 													  merged_indexes,
-													  -1,
 													  default_index);
 		Assert(merged_bounds);
 	}
@@ -2263,6 +2280,8 @@ merge_null_partitions(PartitionMap *outer_map,
 					  int *next_index,
 					  int *null_index)
 {
+	int			outer_merged_index  = outer_map->merged_indexes[outer_null];
+	int			inner_merged_index  = inner_map->merged_indexes[inner_null];
 	bool		consider_outer_null = false;
 	bool		consider_inner_null = false;
 
@@ -2276,13 +2295,13 @@ merge_null_partitions(PartitionMap *outer_map,
 	if (outer_has_null)
 	{
 		Assert(outer_null >= 0 && outer_null < outer_map->nparts);
-		if (outer_map->merged_indexes[outer_null] == -1)
+		if (outer_merged_index == -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)
+		if (inner_merged_index == -1)
 			consider_inner_null = true;
 	}
 
@@ -2626,35 +2645,24 @@ generate_matching_part_pairs(RelOptInfo *outer_rel, RelOptInfo *inner_rel,
 static PartitionBoundInfo
 build_merged_partition_bounds(char strategy, List *merged_datums,
 							  List *merged_isnulls, List *merged_kinds,
-							  List *merged_indexes, int null_index,
+							  List *merged_indexes,
 							  int default_index)
 {
 	PartitionBoundInfo merged_bounds;
 	int			ndatums = list_length(merged_datums);
 	int			pos;
 	ListCell   *lc;
-	int			natts = 1;  //TODO: Handle for multi-column case
-	bool	   *null = NULL;
 
 	merged_bounds = (PartitionBoundInfo) palloc(sizeof(PartitionBoundInfoData));
 	merged_bounds->strategy = strategy;
 
 	if (merged_isnulls)
 	{
-		if (null_index >= 0)
-		{
-			null = (bool *) palloc0(sizeof(bool) * natts);
-			null[0] = true;
-			ndatums++;
-		}
 		merged_bounds->isnulls = (bool **) palloc(sizeof(bool *) * ndatums);
 
 		pos = 0;
 		foreach(lc, merged_isnulls)
 			merged_bounds->isnulls[pos++] = (bool *) lfirst(lc);
-
-		if (null_index >= 0)
-			merged_bounds->isnulls[pos] = null;
 	}
 
 	merged_bounds->ndatums = ndatums;
@@ -2696,9 +2704,6 @@ build_merged_partition_bounds(char strategy, List *merged_datums,
 	foreach(lc, merged_indexes)
 		merged_bounds->indexes[pos++] = lfirst_int(lc);
 
-	if (merged_isnulls && null_index >= 0)
-		merged_bounds->indexes[pos] = null_index;
-
 	merged_bounds->default_index = default_index;
 
 	return merged_bounds;
@@ -3193,7 +3198,6 @@ check_new_partition_bound(char *relname, Relation parent,
 					Assert(boundinfo &&
 						   boundinfo->strategy == PARTITION_STRATEGY_LIST &&
 						   (boundinfo->ndatums > 0 ||
-							partition_bound_accepts_nulls(boundinfo) ||
 							partition_bound_has_default(boundinfo)));
 
 					foreach(cell, spec->listdatums)
@@ -4082,14 +4086,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);
-
-				if (key->partnatts == 1 && 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;
 
@@ -4116,8 +4116,9 @@ make_partition_op_expr(PartitionKey key, int keynum,
 
 					result = (Expr *) saopexpr;
 				}
-				else if (key->partnatts == 1)
+				else if (IsA(arg2, List) && list_length((List *) arg2) > 1)
 				{
+					List	   *elems = (List *) arg2;
 					List	   *elemops = NIL;
 					ListCell   *lc;
 
@@ -4135,14 +4136,16 @@ 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, arg2,
+										   arg1,
+										   IsA(arg2, List) ?
+										   linitial((List *) arg2) : arg2,
 										   InvalidOid,
 										   key->partcollation[keynum]);
 				}
@@ -4262,207 +4265,39 @@ static List *
 get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 {
 	PartitionKey key = RelationGetPartitionKey(parent);
-	List	   *result;
-	Expr	   *keyCol;
-	Expr	   *opexpr;
-	NullTest   *nulltest;
-	ListCell   *cell;
-	List	   *elems = NIL;
-	bool		list_has_null = false;
-
-	if (key->partnatts > 1)
-		return get_qual_for_multi_column_list(parent, spec);
-
-	/* 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));
-
-	/*
-	 * For default list partition, collect datums for all the partitions. The
-	 * default partition constraint should check that the partition key is
-	 * equal to none of those.
-	 */
-	if (spec->is_default)
-	{
-		int			i;
-		int			ndatums = 0;
-		PartitionDesc pdesc = RelationGetPartitionDesc(parent, false);
-		PartitionBoundInfo boundinfo = pdesc->boundinfo;
-
-		if (boundinfo)
-			ndatums = boundinfo->ndatums;
-
-		/*
-		 * If default is the only partition, there need not be any partition
-		 * constraint on it.
-		 */
-		if (ndatums == 0 && !list_has_null)
-			return NIL;
-
-		for (i = 0; i < ndatums; i++)
-		{
-			Const	   *val;
-
-			if (boundinfo->isnulls[i][0])
-			{
-				list_has_null = true;
-				continue;
-			}
-
-			/*
-			 * 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.
-			 */
-			val = makeConst(key->parttypid[0],
-							key->parttypmod[0],
-							key->parttypcoll[0],
-							key->parttyplen[0],
-							datumCopy(boundinfo->datums[i][0],
-									  key->parttypbyval[0],
-									  key->parttyplen[0]),
-							false,	/* isnull */
-							key->parttypbyval[0]);
-
-			elems = lappend(elems, val);
-		}
-	}
-	else
-	{
-		/*
-		 * Create list of Consts for the allowed values, excluding any nulls.
-		 */
-		foreach(cell, spec->listdatums)
-		{
-			ListCell	   *cell2 = NULL;
-
-			foreach(cell2, (List *) lfirst(cell))
-			{
-				Const      *val = castNode(Const, lfirst(cell2));
-
-				if (val->constisnull)
-					list_has_null = true;
-				else
-					elems = lappend(elems, copyObject(val));
-			}
-		}
-	}
-
-	if (elems)
-	{
-		/*
-		 * Generate the operator expression from the non-null partition
-		 * values.
-		 */
-		opexpr = make_partition_op_expr(key, 0, BTEqualStrategyNumber,
-										keyCol, (Expr *) elems);
-	}
-	else
-	{
-		/*
-		 * If there are no partition values, we don't need an operator
-		 * expression.
-		 */
-		opexpr = NULL;
-	}
-
-	if (!list_has_null)
-	{
-		/*
-		 * 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);
-	}
-	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;
-
-		if (opexpr)
-		{
-			Expr	   *or;
-
-			or = makeBoolExpr(OR_EXPR, list_make2(nulltest, opexpr), -1);
-			result = list_make1(or);
-		}
-		else
-			result = list_make1(nulltest);
-	}
-
-	/*
-	 * Note that, in general, applying NOT to a constraint expression doesn't
-	 * necessarily invert the set of rows it accepts, because NOT (NULL) is
-	 * NULL.  However, the partition constraints we construct here never
-	 * evaluate to NULL, so applying NOT works as intended.
-	 */
-	if (spec->is_default)
-	{
-		result = list_make1(make_ands_explicit(result));
-		result = list_make1(makeBoolExpr(NOT_EXPR, result, -1));
-	}
-
-	return result;
-}
-
-/*
- * get_qual_for_list_for_multi_column
- *
- * Returns a list of expressions to use as a list partition's constraint,
- * given the parent relation and partition bound structure.
- *
- * Returns NIL for a default partition when it's the only partition since
- * in that case there is no constraint.
- */
-static List *
-get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
-{
-	int			i = 0;
-	int			j = 0;
-	PartitionKey key = RelationGetPartitionKey(parent);
-	List	   *result;
-	Expr	   *opexpr;
-	NullTest   *nulltest;
+	List	   *result = NIL;
+	Expr	   *datumtest;
+	Expr	   *is_null_test = NULL;
+	List	   *datum_elems = NIL;
 	ListCell   *cell;
-	List	   *elems = NIL;
+	bool		key_is_null[PARTITION_MAX_KEYS];
+	int			i,
+				j;
 	Expr      **keyCol = (Expr **) palloc0 (key->partnatts * sizeof(Expr *));
 
-	/* Construct Var or expression representing the partition columns */
-	for (i = 0; i < key->partnatts; i++)
+	/* 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);
+										 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;
 	}
 
 	/*
@@ -4472,6 +4307,7 @@ get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
 	 */
 	if (spec->is_default)
 	{
+		int			i;
 		int			ndatums = 0;
 		PartitionDesc pdesc = RelationGetPartitionDesc(parent, false);
 		PartitionBoundInfo boundinfo = pdesc->boundinfo;
@@ -4483,40 +4319,42 @@ get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
 		 * If default is the only partition, there need not be any partition
 		 * constraint on it.
 		 */
-		if (ndatums == 0)
+		if (ndatums == 0 && !partition_bound_accepts_nulls(boundinfo))
 			return NIL;
 
 		for (i = 0; i < ndatums; i++)
 		{
-			List       *andexpr = NIL;
+			List	   *and_args = NIL;
+			Expr	   *datum_elem = NULL;
 
+			/*
+			 * For the multi-column case, we must make an BoolExpr that
+			 * ANDs the results of the expressions for various columns,
+			 * where each expresion is either an IS NULL test or an
+			 * OpExpr comparing the column against a non-NULL datum.
+			 */
 			for (j = 0; j < key->partnatts; j++)
 			{
 				Const      *val = NULL;
 
 				if (boundinfo->isnulls[i][j])
 				{
-					nulltest = makeNode(NullTest);
+					NullTest   *nulltest = makeNode(NullTest);
+
+					key_is_null[j] = true;
+
 					nulltest->arg = keyCol[j];
 					nulltest->nulltesttype = IS_NULL;
 					nulltest->argisrow = false;
 					nulltest->location = -1;
-					andexpr = lappend(andexpr, nulltest);
+
+					if (key->partnatts > 1)
+						and_args = lappend(and_args, nulltest);
+					else
+						is_null_test = (Expr *) nulltest;
 				}
 				else
 				{
-					/*
-					 * Gin up a "col IS NOT NULL" test that will be ANDed with
-					 * the each column's expression. This might seem redundant,
-					 * but the partition routing machinery needs it.
-					 */
-					nulltest = makeNode(NullTest);
-					nulltest->arg = keyCol[j];
-					nulltest->nulltesttype = IS_NOT_NULL;
-					nulltest->argisrow = false;
-					nulltest->location = -1;
-					andexpr = lappend(andexpr, nulltest);
-
 					val = makeConst(key->parttypid[j],
 									key->parttypmod[j],
 									key->parttypcoll[j],
@@ -4527,68 +4365,143 @@ get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
 									false,  /* isnull */
 									key->parttypbyval[j]);
 
-					opexpr = make_partition_op_expr(key, j, BTEqualStrategyNumber,
-													keyCol[j], (Expr *) val);
-					andexpr = lappend(andexpr, opexpr);
+					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;
 				}
 			}
 
-			opexpr = makeBoolExpr(AND_EXPR, andexpr, -1);
-			elems = lappend(elems, opexpr);
+			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.
-		 */
 		foreach(cell, spec->listdatums)
 		{
-			List	   *andexpr = NIL;
-			ListCell   *cell2 = NULL;
+			List	   *listbound = (List *) lfirst(cell);
+			ListCell   *cell2;
+			List	   *and_args = NIL;
+			Expr	   *datum_elem = NULL;
 
+			/*
+			 * See the comment above regarding the handling for the
+			 * multi-column case.
+			 */
 			j = 0;
-			foreach(cell2, (List *) lfirst(cell))
+			foreach(cell2, listbound)
 			{
 				Const      *val = castNode(Const, lfirst(cell2));
 
 				if (val->constisnull)
 				{
-					nulltest = makeNode(NullTest);
+					NullTest   *nulltest = makeNode(NullTest);
+
+					key_is_null[j] = true;
+
 					nulltest->arg = keyCol[j];
 					nulltest->nulltesttype = IS_NULL;
 					nulltest->argisrow = false;
 					nulltest->location = -1;
-					andexpr = lappend(andexpr, nulltest);
+
+					if (key->partnatts > 1)
+						and_args = lappend(and_args, nulltest);
+					else
+						is_null_test = (Expr *) nulltest;
 				}
 				else
 				{
-					/*
-					 * Gin up a "col IS NOT NULL" test that will be ANDed with
-					 * the each column's expression. This might seem redundant,
-					 * but the partition routing machinery needs it.
-					 */
-					nulltest = makeNode(NullTest);
-					nulltest->arg = keyCol[j];
-					nulltest->nulltesttype = IS_NOT_NULL;
-					nulltest->argisrow = false;
-					nulltest->location = -1;
-					andexpr = lappend(andexpr, nulltest);
-
-					opexpr = make_partition_op_expr(key, j, BTEqualStrategyNumber,
-													keyCol[j], (Expr *) val);
-					andexpr = lappend(andexpr, opexpr);
+					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++;
 			}
 
-			opexpr = makeBoolExpr(AND_EXPR, andexpr, -1);
-			elems = lappend(elems, opexpr);
+			if (list_length(and_args) > 1)
+				datum_elem = makeBoolExpr(AND_EXPR, and_args, -1);
+
+			if (datum_elem)
+				datum_elems = lappend(datum_elems, datum_elem);
 		}
 	}
 
-	opexpr = makeBoolExpr(OR_EXPR, elems, -1);
-	result = list_make1(opexpr);
+	/*
+	 * 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 (!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);
+		}
+	}
+
+	/*
+	 * 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)
+	{
+		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
+		datumtest = NULL;
+
+	/*
+	 * 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);
+
+		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
@@ -4597,7 +4510,10 @@ get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
 	 * evaluate to NULL, so applying NOT works as intended.
 	 */
 	if (spec->is_default)
+	{
+		result = list_make1(make_ands_explicit(result));
 		result = list_make1(makeBoolExpr(NOT_EXPR, result, -1));
+	}
 
 	return result;
 }
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index 76ea26c89e..850ee09995 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -136,7 +136,13 @@ typedef struct PruneStepResult
 	Bitmapset  *bound_offsets;
 
 	bool		scan_default;	/* Scan the default partition? */
-	bool		scan_null;		/* Scan the partition for NULL values? */
+
+	/*
+	 * This records the key column cardinal positions for which a IS NULL
+	 * clause was specified in the step (or constituent steps if this is
+	 * a combine step).
+	 */
+	Bitmapset  *nullkeys;
 } PruneStepResult;
 
 
@@ -906,12 +912,15 @@ get_matching_partitions(PartitionPruneContext *context, List *pruning_steps)
 	}
 
 	/* Add the null and/or default partition if needed and present. */
-	if (final_result->scan_null)
+	if (!bms_is_empty(final_result->nullkeys))
 	{
+		Bitmapset *null_parts;
+
 		Assert(context->strategy == PARTITION_STRATEGY_LIST);
 		Assert(partition_bound_accepts_nulls(context->boundinfo));
-		result = bms_add_member(result,
-								get_partition_bound_null_index(context->boundinfo));
+		null_parts = get_partition_bound_null_indexes(context->boundinfo,
+													  final_result->nullkeys);
+		result = bms_add_members(result, null_parts);
 	}
 	if (scan_default)
 	{
@@ -2662,10 +2671,11 @@ get_matching_hash_bounds(PartitionPruneContext *context,
 	}
 
 	/*
-	 * There is neither a special hash null partition or the default hash
-	 * partition.
+	 * Hash partitioning doesn't really store which partitions accept NULLs
+	 * in which keys, nor is there the default hash partition.
 	 */
-	result->scan_null = result->scan_default = false;
+	result->nullkeys = NULL;
+	result->scan_default = false;
 
 	return result;
 }
@@ -2839,8 +2849,8 @@ add_partitions(PruneStepResult *result, bool **isnulls, int minoff, int maxoff,
  *		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.  'nullkeys' is set
+ * to the one passed by the caller.
  *
  * 'opstrategy' if non-zero must be a btree strategy number.
  *
@@ -2870,7 +2880,8 @@ get_matching_list_bounds(PartitionPruneContext *context,
 
 	Assert(context->strategy == PARTITION_STRATEGY_LIST);
 
-	result->scan_null = result->scan_default = false;
+	result->nullkeys = nullkeys;
+	result->scan_default = false;
 
 	/*
 	 * If there are no datums to compare keys with, but there are partitions,
@@ -3102,7 +3113,8 @@ get_matching_range_bounds(PartitionPruneContext *context,
 	Assert(context->strategy == PARTITION_STRATEGY_RANGE);
 	Assert(nvalues <= partnatts);
 
-	result->scan_null = result->scan_default = false;
+	result->nullkeys = NULL;
+	result->scan_default = false;
 
 	/*
 	 * If there are no datums to compare keys with, or if we got an IS NULL
@@ -3619,7 +3631,7 @@ perform_pruning_base_step(PartitionPruneContext *context,
 				result = (PruneStepResult *) palloc(sizeof(PruneStepResult));
 				result->bound_offsets = NULL;
 				result->scan_default = false;
-				result->scan_null = false;
+				result->nullkeys = NULL;
 
 				return result;
 			}
@@ -3729,7 +3741,13 @@ 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);
+
+		/*
+		 * All partitions, including those that accept NULL for some partition
+		 * key, have been included, so no need to be accurate about setting
+		 * nullkeys.
+		 */
+		result->nullkeys = NULL;
 		return result;
 	}
 
@@ -3757,8 +3775,8 @@ perform_pruning_combine_step(PartitionPruneContext *context,
 														step_result->bound_offsets);
 
 				/* Update whether to scan null and default partitions. */
-				if (!result->scan_null)
-					result->scan_null = step_result->scan_null;
+				if (bms_is_empty(result->nullkeys))
+					result->nullkeys = step_result->nullkeys;
 				if (!result->scan_default)
 					result->scan_default = step_result->scan_default;
 			}
@@ -3781,7 +3799,7 @@ 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->nullkeys = step_result->nullkeys;
 					result->scan_default = step_result->scan_default;
 					firststep = false;
 				}
@@ -3793,8 +3811,8 @@ perform_pruning_combine_step(PartitionPruneContext *context,
 										step_result->bound_offsets);
 
 					/* Update whether to scan null and default partitions. */
-					if (result->scan_null)
-						result->scan_null = step_result->scan_null;
+					result->nullkeys = bms_union(result->nullkeys,
+												 step_result->nullkeys);
 					if (result->scan_default)
 						result->scan_default = step_result->scan_default;
 				}
diff --git a/src/include/partitioning/partbounds.h b/src/include/partitioning/partbounds.h
index 9b5ab7270a..16f2fe60bc 100644
--- a/src/include/partitioning/partbounds.h
+++ b/src/include/partitioning/partbounds.h
@@ -80,6 +80,7 @@ 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;
@@ -98,7 +99,8 @@ typedef struct PartitionBoundInfoData
 #define partition_bound_has_default(bi) ((bi)->default_index != -1)
 
 extern bool partition_bound_accepts_nulls(PartitionBoundInfo boundinfo);
-extern int get_partition_bound_null_index(PartitionBoundInfo boundinfo);
+extern Bitmapset *get_partition_bound_null_indexes(PartitionBoundInfo boundinfo,
+											Bitmapset *nullkeys);
 
 extern int	get_hash_partition_greatest_modulus(PartitionBoundInfo b);
 extern uint64 compute_partition_hash_value(int partnatts, FmgrInfo *partsupfunc,
diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out
index 158c1d9d63..038cc5395e 100644
--- a/src/test/regress/expected/insert.out
+++ b/src/test/regress/expected/insert.out
@@ -1067,7 +1067,7 @@ Partitions: mclparted_p1 FOR VALUES IN (('a', 1)),
  a      | text    |           |          |         | extended |              | 
  b      | integer |           |          |         | plain    |              | 
 Partition of: mclparted FOR VALUES IN (('a', 1))
-Partition constraint: (((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL) AND (b = 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"
@@ -1076,7 +1076,7 @@ Partition constraint: (((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL)
  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 (a = 'a'::text) AND (b IS NOT NULL) AND (b = 2)) OR ((a IS NOT NULL) AND (a = 'b'::text) AND (b IS NOT NULL) AND (b = 1)) OR ((a IS NOT NULL) AND (a = 'c'::text) AND (b IS NOT NULL) AND (b = 3)) OR ((a IS NOT NULL) AND (a = 'd'::text) AND (b IS NOT NULL) AND (b = 3)) OR ((a IS NOT NULL) AND (a = 'e'::text) AND (b IS NOT NULL) AND (b = 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"
@@ -1085,7 +1085,7 @@ Partition constraint: (((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL)
  a      | text    |           |          |         | extended |              | 
  b      | integer |           |          |         | plain    |              | 
 Partition of: mclparted FOR VALUES IN (('a', 3), ('a', 4), ('a', NULL), (NULL, 1))
-Partition constraint: (((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL) AND (b = 3)) OR ((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL) AND (b = 4)) OR ((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NULL)) OR ((a IS NULL) AND (b IS NOT NULL) AND (b = 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"
@@ -1094,7 +1094,7 @@ Partition constraint: (((a IS NOT NULL) AND (a = 'a'::text) AND (b IS NOT NULL)
  a      | text    |           |          |         | extended |              | 
  b      | integer |           |          |         | plain    |              | 
 Partition of: mclparted FOR VALUES IN (('b', NULL), (NULL, 2))
-Partition constraint: (((a IS NOT NULL) AND (a = 'b'::text) AND (b IS NULL)) OR ((a IS NULL) AND (b IS NOT NULL) AND (b = 2)))
+Partition constraint: (((a = 'b'::text) AND (b IS NULL)) OR ((a IS NULL) AND (b = 2)))
 
 \d+ mclparted_p5
                                 Table "public.mclparted_p5"
#22Zhihong Yu
zyu@yugabyte.com
In reply to: Amit Langote (#21)
Re: Multi-Column List Partitioning

On Fri, Oct 1, 2021 at 6:56 AM Amit Langote <amitlangote09@gmail.com> wrote:

Hi,

On Mon, Sep 13, 2021 at 7:17 PM Rajkumar Raghuwanshi
<rajkumar.raghuwanshi@enterprisedb.com> wrote:

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is

failing with below errors.

Thanks Rajkumar for testing.

--inherit.sql is failing with error :"ERROR: negative bitmapset member

not allowed"

update mlparted_tab mlp set c = 'xxx'
from
(select a from some_tab union all select a+1 from some_tab) ss (a)
where (mlp.a = ss.a and mlp.b = 'b') or mlp.a = 3;
ERROR: negative bitmapset member not allowed

--partition_join.sql is crashing with enable_partitionwise_join set to

true.

Here's a v2 of the delta patch that should fix both of these test
failures. As I mentioned in my last reply, my delta patch fixed what
I think were problems in Nitin's v3 patch but were not complete by
themselves. Especially, I hadn't bothered to investigate various /*
TODO: handle multi-column list partitioning */ sites to deal with my
own changes.

In the attached updated version, I've dealt with some of those such
that at least the existing cases exercising partition pruning and
partition wise joins now pass.

I thought about sending a v4 of the main patch with my proposed
changes so far integrated, but decided to just post a delta_v2 for
now.

--
Amit Langote
EDB: http://www.enterprisedb.com

Hi,

+           for (i = 0; i < partnatts; i++)
+           {
+               if (outer_isnull[i])
+                   outer_has_null = true;

We can come out of the loop once outer_has_null is assigned.
Similar comment for the loop inside if (inner_isnull).

+ * For the multi-column case, we must make an BoolExpr that

an BoolExpr -> a BoolExpr

Cheers

#23Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Rajkumar Raghuwanshi (#20)
1 attachment(s)
Re: Multi-Column List Partitioning

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is failing with below errors.

Thanks Rajkumar for testing.

Here's a v2 of the delta patch that should fix both of these test
failures. As I mentioned in my last reply, my delta patch fixed what
I think were problems in Nitin's v3 patch but were not complete by
themselves. Especially, I hadn't bothered to investigate various /*
TODO: handle multi-column list partitioning */ sites to deal with my
own changes.

Thanks Rajkumar for testing and Thank you Amit for working on v2 of
the delta patch. Actually I had done the code changes related to
partition-wise join and I was in the middle of fixing the review
comments, So I could not share the patch. Anyways thanks for your
efforts.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b, c);
create table p01 partition of p0 for values in ((1, 1, true), (NULL, 1, false));
create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

In the attached updated version, I've dealt with some of those such
that at least the existing cases exercising partition pruning and
partition wise joins now pass.

wrt partition pruning, I have checked the output of the above case
with the v2 version of the delta patch and without that. The output
remains same. Kindly let me know if I am missing something. But I feel
the above output is correct as the partition p01 is the only partition
which contains NULL value for column a, hence it is showing "Seq scan
on p01" in the output. Kindly correct me if I am wrong. I feel the
code changes related to 'null_keys' is not required, hence not
incorporated that in the attached patch.

wrt partition-wise join, I had run the regression test (with new cases
related to partition-wise join) on v2 of the delta patch and observed
the crash. Hence I have not incorporated the partition-wise join
related code from v2 of delta patch to main v4 patch. Instead I have
added the partition-wise join related code done by me in the attached
patch. Please share your thoughts and if possible we can improvise the
code. Rest of the changes looks good to me and I have incorporated
that in the attached patch.

I guess that may be due to the following newly added code being incomplete:
Maybe this function needs to return a "bitmapset" of indexes, because
multiple partitions can now contain NULL values.

I feel this function is not required at all as we are not separating
the non null and null partitions now. Removed in the attached patch.
Also removed the "scan_null' variable from the structure
"PruneStepResult" and cleaned up the corresponding code blocks.

This function name may be too generic. Given that it is specific to
implementing list bound de-duplication, maybe the following signature
is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Yes. The function name looks more generic. How about using
"isListBoundDuplicated()"? I have used this name in the patch. Please
let me know if that does not look correct.

Also, better if the function comment mentions those parameter names, like:

"Returns TRUE if the list bound element 'new_bound' is already present
in the target list 'list_bounds', FALSE otherwise."

Fixed.

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw grammar
+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

Fixed.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

Fixed in all the places.

+           colname[i] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd statement
overwrites its pointer by the pointer to the string palloc'd by
get_attname().

Removed the 1st statement as it is not required.

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

Fixed in all the places.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate, exprLocation((Node
*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

Fixed.

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how about
writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

This looks good. I have changed in the patch.

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Yes. PartitionListBound looks more appropriate and it also matches the
similar structures of the other partition strategies.

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Retained the old comment.

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
*/

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

Fixed.

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+           all_values[j]->values = (Datum *) palloc0(key->partnatts *
sizeof(Datum));
+           all_values[j]->isnulls = (bool *) palloc0(key->partnatts *
sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts * sizeof(Datum));

Missing a newline between these two statements.

Fixed. Made necessary changes to keep the intent of existing code.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in partition_bounds_equal()
needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously mentioned.

As mentioned earlier, removed the functionality of
get_partition_bound_null_index(), hence the above condition is not
required and removed.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the above
code stanza which appears to implement a short-cut for this long-form
logic.

Yes. May be we could have ignored the above code stanza if we would
have comparing the null indexes using get_partition_bound_null_index()
in the beginning of the function. But hence we are not separating the
non null partitions and null partitions, I would like to keep the
logic in the inner loop as we are doing it for non null bound values
in the above code stanza, just to give a feel that null bound values
are also handled the same way as non null values. Please correct me if
I am wrong.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

Fixed.

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

Thanks for the suggestion. I have changed as above.

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the logic
for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the same
goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

Actually I had written a separate function because of the complexity.
Now I have understood that since the objective is same, it should be
done in a single function irrespective of complexity.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits the
IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a few
other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

Thank you very much for your efforts. The changes looks good to me and
I have incorporated these changes in the attached patch.

I have completed the coding for all the TODOs and hence removed in the
patch. The naming conventions used for function/variable names varies
across the files. Some places it is like 'namesLikeThis' and in some
place it is like 'names_like_this'. I have used the naming conventions
based on the surrounding styles used. I am happy to change those if
required.

I have verified 'make check' with the attached patch and it is working fine.

Thanks & Regards,
Nitin Jadhav

On Mon, Sep 13, 2021 at 3:47 PM Rajkumar Raghuwanshi
<rajkumar.raghuwanshi@enterprisedb.com> wrote:

Show quoted text

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is failing with below errors.

--inherit.sql is failing with error :"ERROR: negative bitmapset member not allowed"
update mlparted_tab mlp set c = 'xxx'
from
(select a from some_tab union all select a+1 from some_tab) ss (a)
where (mlp.a = ss.a and mlp.b = 'b') or mlp.a = 3;
ERROR: negative bitmapset member not allowed

--partition_join.sql is crashing with enable_partitionwise_join set to true.
CREATE TABLE plt1_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt1_adv_p1 PARTITION OF plt1_adv FOR VALUES IN ('0001', '0003');
CREATE TABLE plt1_adv_p2 PARTITION OF plt1_adv FOR VALUES IN ('0004', '0006');
CREATE TABLE plt1_adv_p3 PARTITION OF plt1_adv FOR VALUES IN ('0008', '0009');
INSERT INTO plt1_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM generate_series(1, 299) i WHERE i % 10 IN (1, 3, 4, 6, 8, 9);
ANALYZE plt1_adv;
CREATE TABLE plt2_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt2_adv_p1 PARTITION OF plt2_adv FOR VALUES IN ('0002', '0003');
CREATE TABLE plt2_adv_p2 PARTITION OF plt2_adv FOR VALUES IN ('0004', '0006');
CREATE TABLE plt2_adv_p3 PARTITION OF plt2_adv FOR VALUES IN ('0007', '0009');
INSERT INTO plt2_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM generate_series(1, 299) i WHERE i % 10 IN (2, 3, 4, 6, 7, 9);
ANALYZE plt2_adv;
-- inner join
EXPLAIN (COSTS OFF)
SELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c = t2.c) WHERE t1.b < 10 ORDER BY t1.a;
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost

--stack-trace
Core was generated by `postgres: edb regression [local] EXPLAIN '.
Program terminated with signal 6, Aborted.
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install glibc-2.17-222.el7.x86_64 keyutils-libs-1.5.8-3.el7.x86_64 krb5-libs-1.15.1-19.el7.x86_64 libcom_err-1.42.9-12.el7_5.x86_64 libgcc-4.8.5-39.el7.x86_64 libselinux-2.5-12.el7.x86_64 openssl-libs-1.0.2k-19.el7.x86_64 pcre-8.32-17.el7.x86_64 zlib-1.2.7-17.el7.x86_64
(gdb) bt
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
#1 0x00007f7d339bb968 in abort () from /lib64/libc.so.6
#2 0x0000000000b0fbc3 in ExceptionalCondition (conditionName=0xcbda10 "part_index >= 0", errorType=0xcbd1c3 "FailedAssertion", fileName=0xcbd2fe "partbounds.c", lineNumber=1957)
at assert.c:69
#3 0x0000000000892aa1 in is_dummy_partition (rel=0x19b37c0, part_index=-1) at partbounds.c:1957
#4 0x00000000008919bd in merge_list_bounds (partnatts=1, partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0, inner_rel=0x1922938, jointype=JOIN_INNER,
outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at partbounds.c:1529
#5 0x00000000008910de in partition_bounds_merge (partnatts=1, partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0, inner_rel=0x1922938, jointype=JOIN_INNER,
outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at partbounds.c:1223
#6 0x000000000082c41a in compute_partition_bounds (root=0x1a19ed0, rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30, parent_sjinfo=0x7fffd67752a0, parts1=0x7fffd67751b0,
parts2=0x7fffd67751a8) at joinrels.c:1644
#7 0x000000000082bc34 in try_partitionwise_join (root=0x1a19ed0, rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30, parent_sjinfo=0x7fffd67752a0, parent_restrictlist=0x1ab3318)
at joinrels.c:1402
#8 0x000000000082aea2 in populate_joinrel_with_paths (root=0x1a19ed0, rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30, sjinfo=0x7fffd67752a0, restrictlist=0x1ab3318)
at joinrels.c:926
#9 0x000000000082a8f5 in make_join_rel (root=0x1a19ed0, rel1=0x19b37c0, rel2=0x1922938) at joinrels.c:760
#10 0x0000000000829e03 in make_rels_by_clause_joins (root=0x1a19ed0, old_rel=0x19b37c0, other_rels_list=0x1ab2970, other_rels=0x1ab2990) at joinrels.c:312
#11 0x00000000008298d9 in join_search_one_level (root=0x1a19ed0, level=2) at joinrels.c:123
#12 0x000000000080c566 in standard_join_search (root=0x1a19ed0, levels_needed=2, initial_rels=0x1ab2970) at allpaths.c:3020
#13 0x000000000080c4df in make_rel_from_joinlist (root=0x1a19ed0, joinlist=0x199d538) at allpaths.c:2951
#14 0x000000000080816b in make_one_rel (root=0x1a19ed0, joinlist=0x199d538) at allpaths.c:228
#15 0x000000000084491d in query_planner (root=0x1a19ed0, qp_callback=0x84a538 <standard_qp_callback>, qp_extra=0x7fffd6775630) at planmain.c:276
#16 0x0000000000847040 in grouping_planner (root=0x1a19ed0, tuple_fraction=0) at planner.c:1447
#17 0x0000000000846709 in subquery_planner (glob=0x19b39d8, parse=0x1aaa290, parent_root=0x0, hasRecursion=false, tuple_fraction=0) at planner.c:1025
#18 0x0000000000844f3e in standard_planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c = t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048, boundParams=0x0) at planner.c:406
#19 0x0000000000844ce9 in planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c = t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048, boundParams=0x0) at planner.c:277
#20 0x0000000000978483 in pg_plan_query (querytree=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c = t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048, boundParams=0x0) at postgres.c:847
#21 0x00000000006937fc in ExplainOneQuery (query=0x1aaa290, cursorOptions=2048, into=0x0, es=0x19b36f0,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c = t2.c) WHERE t1.b < 10 ORDER BY t1.a;",
params=0x0, queryEnv=0x0) at explain.c:397
#22 0x0000000000693351 in ExplainQuery (pstate=0x197c410, stmt=0x1aaa0b0, params=0x0, dest=0x197c378) at explain.c:281
#23 0x00000000009811fa in standard_ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c = t2.c) WHERE t1.b < 10 ORDER BY t1.a;",
readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0, queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at utility.c:845
#24 0x00000000009809ec in ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c = t2.c) WHERE t1.b < 10 ORDER BY t1.a;",
readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0, queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at utility.c:527
#25 0x000000000097f636 in PortalRunUtility (portal=0x1893b40, pstmt=0x1a0bfc8, isTopLevel=true, setHoldSnapshot=true, dest=0x197c378, qc=0x7fffd6775f90) at pquery.c:1147
#26 0x000000000097f3a5 in FillPortalStore (portal=0x1893b40, isTopLevel=true) at pquery.c:1026
#27 0x000000000097ed11 in PortalRun (portal=0x1893b40, count=9223372036854775807, isTopLevel=true, run_once=true, dest=0x1a0c0b8, altdest=0x1a0c0b8, qc=0x7fffd6776150) at pquery.c:758
#28 0x0000000000978aa5 in exec_simple_query (

Thanks & Regards,
Rajkumar Raghuwanshi

On Fri, Sep 3, 2021 at 7:17 PM Amit Langote <amitlangote09@gmail.com> wrote:

On Wed, Sep 1, 2021 at 2:31 PM Amit Langote <amitlangote09@gmail.com> wrote:

On Tue, Aug 31, 2021 at 8:02 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

The attached patch also fixes the above comments.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b, c);
create table p01 partition of p0 for values in ((1, 1, true), (NULL, 1, false));
create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

I guess that may be due to the following newly added code being incomplete:

+/*
+ * get_partition_bound_null_index
+ *
+ * Returns the partition index of the partition bound which accepts NULL.
+ */
+int
+get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+{
+   int i = 0;
+   int j = 0;
+
+   if (!boundinfo->isnulls)
+       return -1;
-           if (!val->constisnull)
-               count++;
+   for (i = 0; i < boundinfo->ndatums; i++)
+   {
+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)
+       {
+           if (boundinfo->isnulls[i][j])
+               return boundinfo->indexes[i];
}
}

+ return -1;
+}

Maybe this function needs to return a "bitmapset" of indexes, because
multiple partitions can now contain NULL values.

Some other issues I noticed and suggestions for improvement:

+/*
+ * checkForDuplicates
+ *
+ * Returns TRUE if the list bound element is already present in the list of
+ * list bounds, FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)

This function name may be too generic. Given that it is specific to
implementing list bound de-duplication, maybe the following signature
is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Also, better if the function comment mentions those parameter names, like:

"Returns TRUE if the list bound element 'new_bound' is already present
in the target list 'list_bounds', FALSE otherwise."

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw grammar
+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

+           colname[i] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd statement
overwrites its pointer by the pointer to the string palloc'd by
get_attname().

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate, exprLocation((Node
*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how about
writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Will send more comments in a bit...

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
*/

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+           all_values[j]->values = (Datum *) palloc0(key->partnatts *
sizeof(Datum));
+           all_values[j]->isnulls = (bool *) palloc0(key->partnatts *
sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts * sizeof(Datum));

Missing a newline between these two statements.

BTW, I noticed that the boundDatums variable is no longer used in
create_list_bounds. I traced back its origin and found that a recent
commit 53d86957e98 introduced it to implement an idea to reduce the
finer-grained pallocs that were being done in create_list_bounds(). I
don't think that this patch needs to throw away that work. You can
make it work as the attached delta patch that applies on top of v3.
Please check.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in partition_bounds_equal()
needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously mentioned.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the above
code stanza which appears to implement a short-cut for this long-form
logic.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

(Skipped looking at merge_list_bounds() and related changes for now as
I see a lot of TODOs remain to be done.)

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the logic
for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the same
goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits the
IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a few
other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

I still have more than half of v3 left to look at, so will continue
looking. In the meantime, please check the changes I suggested,
including the delta patch, and let me know your thoughts.

--
Amit Langote
EDB: http://www.enterprisedb.com

Attachments:

v4-0001-multi-column-list-partitioning.patchapplication/octet-stream; name=v4-0001-multi-column-list-partitioning.patchDownload
From 9aed8fcba96dd35503826313b1bba2871505bcfd Mon Sep 17 00:00:00 2001
From: Nitin <nitin.jadhav@enterprisedb.com>
Date: Sun, 3 Oct 2021 01:47:11 +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            |  191 +++-
 src/backend/partitioning/partbounds.c         |  879 ++++++++++-------
 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    |   34 +-
 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         |   25 +-
 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, 3331 insertions(+), 545 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ff97b61..35761dc 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..fd39922 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,105 @@ 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 (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))));
+
+			if (partnatts != list_length(rowexpr->args))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("Must specify exactly one value per partitioning column"),
+						 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..98a800f 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,27 @@ 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)
 			{
+				/* 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 +1471,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 +1485,27 @@ 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)
 			{
+				/* 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 +1520,11 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 													   &default_index);
 				if (merged_index == -1)
 					goto cleanup;
-				merged_datum = inner_datums;
 			}
 
+			merged_datum = outer_datums;
+			merged_isnull = outer_isnull;
+
 			/* Move to the next list value on the inner side. */
 			inner_pos++;
 		}
@@ -1422,29 +1537,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 +1574,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 +1585,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 +1894,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 +2253,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 +2282,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 +2297,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 +2320,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 +2599,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 +2610,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 +2638,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 +2650,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 +3157,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 +3696,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 +3747,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 +3761,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 +3934,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 +4038,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 +4068,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 +4088,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 +4217,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 +4265,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 b15bd81..edd6a85 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..5e7187f 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,
@@ -919,6 +913,34 @@ 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;
+-- 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..61e1129 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,
@@ -728,6 +722,25 @@ 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;
+
+-- 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

#24Rajkumar Raghuwanshi
rajkumar.raghuwanshi@enterprisedb.com
In reply to: Nitin Jadhav (#23)
Re: Multi-Column List Partitioning

Thanks Nitin,

v4 patches applied cleanly and make check is passing now. While testing
further I observed that if multiple values are given for a single
column list partition it is not giving error instead it is changing values
itself. Please find the example below.

postgres=# CREATE TABLE plt1 (a int, b varchar) PARTITION BY LIST(b);
CREATE TABLE
postgres=# CREATE TABLE plt1_p1 PARTITION OF plt1 FOR VALUES IN
(('0001','0001'),('0002','0002'));
CREATE TABLE
postgres=# \d+ plt1;
Partitioned table "public.plt1"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
a | integer | | | | plain |
| |
b | character varying | | | | extended |
| |
Partition key: LIST (b)
Partitions: plt1_p1 FOR VALUES IN ('(0001,0001)', '(0002,0002)')

I think it should throw an error as the partition by list has only 1 column
but we are giving 2 values.
also if you see \d+ showing plt1_p1 partition value as ‘(0001,0001)’
instead of ('0001','0001').

Thanks & Regards,
Rajkumar Raghuwanshi

On Sun, Oct 3, 2021 at 1:52 AM Nitin Jadhav <nitinjadhavpostgres@gmail.com>
wrote:

Show quoted text

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is

failing with below errors.

Thanks Rajkumar for testing.

Here's a v2 of the delta patch that should fix both of these test
failures. As I mentioned in my last reply, my delta patch fixed what
I think were problems in Nitin's v3 patch but were not complete by
themselves. Especially, I hadn't bothered to investigate various /*
TODO: handle multi-column list partitioning */ sites to deal with my
own changes.

Thanks Rajkumar for testing and Thank you Amit for working on v2 of
the delta patch. Actually I had done the code changes related to
partition-wise join and I was in the middle of fixing the review
comments, So I could not share the patch. Anyways thanks for your
efforts.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b, c);
create table p01 partition of p0 for values in ((1, 1, true), (NULL, 1,

false));

create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

In the attached updated version, I've dealt with some of those such
that at least the existing cases exercising partition pruning and
partition wise joins now pass.

wrt partition pruning, I have checked the output of the above case
with the v2 version of the delta patch and without that. The output
remains same. Kindly let me know if I am missing something. But I feel
the above output is correct as the partition p01 is the only partition
which contains NULL value for column a, hence it is showing "Seq scan
on p01" in the output. Kindly correct me if I am wrong. I feel the
code changes related to 'null_keys' is not required, hence not
incorporated that in the attached patch.

wrt partition-wise join, I had run the regression test (with new cases
related to partition-wise join) on v2 of the delta patch and observed
the crash. Hence I have not incorporated the partition-wise join
related code from v2 of delta patch to main v4 patch. Instead I have
added the partition-wise join related code done by me in the attached
patch. Please share your thoughts and if possible we can improvise the
code. Rest of the changes looks good to me and I have incorporated
that in the attached patch.

I guess that may be due to the following newly added code being

incomplete:

Maybe this function needs to return a "bitmapset" of indexes, because
multiple partitions can now contain NULL values.

I feel this function is not required at all as we are not separating
the non null and null partitions now. Removed in the attached patch.
Also removed the "scan_null' variable from the structure
"PruneStepResult" and cleaned up the corresponding code blocks.

This function name may be too generic. Given that it is specific to
implementing list bound de-duplication, maybe the following signature
is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Yes. The function name looks more generic. How about using
"isListBoundDuplicated()"? I have used this name in the patch. Please
let me know if that does not look correct.

Also, better if the function comment mentions those parameter names,

like:

"Returns TRUE if the list bound element 'new_bound' is already present
in the target list 'list_bounds', FALSE otherwise."

Fixed.

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw

grammar

+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

Fixed.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

Fixed in all the places.

+           colname[i] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd statement
overwrites its pointer by the pointer to the string palloc'd by
get_attname().

Removed the 1st statement as it is not required.

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

Fixed in all the places.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate, exprLocation((Node
*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

Fixed.

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how about
writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

This looks good. I have changed in the patch.

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Yes. PartitionListBound looks more appropriate and it also matches the
similar structures of the other partition strategies.

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Retained the old comment.

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
*/

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

Fixed.

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+           all_values[j]->values = (Datum *) palloc0(key->partnatts *
sizeof(Datum));
+           all_values[j]->isnulls = (bool *) palloc0(key->partnatts *
sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts *

sizeof(Datum));

Missing a newline between these two statements.

Fixed. Made necessary changes to keep the intent of existing code.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in partition_bounds_equal()
needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously mentioned.

As mentioned earlier, removed the functionality of
get_partition_bound_null_index(), hence the above condition is not
required and removed.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the above
code stanza which appears to implement a short-cut for this long-form
logic.

Yes. May be we could have ignored the above code stanza if we would
have comparing the null indexes using get_partition_bound_null_index()
in the beginning of the function. But hence we are not separating the
non null partitions and null partitions, I would like to keep the
logic in the inner loop as we are doing it for non null bound values
in the above code stanza, just to give a feel that null bound values
are also handled the same way as non null values. Please correct me if
I am wrong.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

Fixed.

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

Thanks for the suggestion. I have changed as above.

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the logic
for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the same
goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

Actually I had written a separate function because of the complexity.
Now I have understood that since the objective is same, it should be
done in a single function irrespective of complexity.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits the
IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a few
other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

Thank you very much for your efforts. The changes looks good to me and
I have incorporated these changes in the attached patch.

I have completed the coding for all the TODOs and hence removed in the
patch. The naming conventions used for function/variable names varies
across the files. Some places it is like 'namesLikeThis' and in some
place it is like 'names_like_this'. I have used the naming conventions
based on the surrounding styles used. I am happy to change those if
required.

I have verified 'make check' with the attached patch and it is working
fine.

Thanks & Regards,
Nitin Jadhav

On Mon, Sep 13, 2021 at 3:47 PM Rajkumar Raghuwanshi
<rajkumar.raghuwanshi@enterprisedb.com> wrote:

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is

failing with below errors.

--inherit.sql is failing with error :"ERROR: negative bitmapset member

not allowed"

update mlparted_tab mlp set c = 'xxx'
from
(select a from some_tab union all select a+1 from some_tab) ss (a)
where (mlp.a = ss.a and mlp.b = 'b') or mlp.a = 3;
ERROR: negative bitmapset member not allowed

--partition_join.sql is crashing with enable_partitionwise_join set to

true.

CREATE TABLE plt1_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt1_adv_p1 PARTITION OF plt1_adv FOR VALUES IN ('0001',

'0003');

CREATE TABLE plt1_adv_p2 PARTITION OF plt1_adv FOR VALUES IN ('0004',

'0006');

CREATE TABLE plt1_adv_p3 PARTITION OF plt1_adv FOR VALUES IN ('0008',

'0009');

INSERT INTO plt1_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM

generate_series(1, 299) i WHERE i % 10 IN (1, 3, 4, 6, 8, 9);

ANALYZE plt1_adv;
CREATE TABLE plt2_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt2_adv_p1 PARTITION OF plt2_adv FOR VALUES IN ('0002',

'0003');

CREATE TABLE plt2_adv_p2 PARTITION OF plt2_adv FOR VALUES IN ('0004',

'0006');

CREATE TABLE plt2_adv_p3 PARTITION OF plt2_adv FOR VALUES IN ('0007',

'0009');

INSERT INTO plt2_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM

generate_series(1, 299) i WHERE i % 10 IN (2, 3, 4, 6, 7, 9);

ANALYZE plt2_adv;
-- inner join
EXPLAIN (COSTS OFF)
SELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON

(t1.a = t2.a AND t1.c = t2.c) WHERE t1.b < 10 ORDER BY t1.a;

server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost

--stack-trace
Core was generated by `postgres: edb regression [local] EXPLAIN

'.

Program terminated with signal 6, Aborted.
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install

glibc-2.17-222.el7.x86_64 keyutils-libs-1.5.8-3.el7.x86_64
krb5-libs-1.15.1-19.el7.x86_64 libcom_err-1.42.9-12.el7_5.x86_64
libgcc-4.8.5-39.el7.x86_64 libselinux-2.5-12.el7.x86_64
openssl-libs-1.0.2k-19.el7.x86_64 pcre-8.32-17.el7.x86_64
zlib-1.2.7-17.el7.x86_64

(gdb) bt
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
#1 0x00007f7d339bb968 in abort () from /lib64/libc.so.6
#2 0x0000000000b0fbc3 in ExceptionalCondition (conditionName=0xcbda10

"part_index >= 0", errorType=0xcbd1c3 "FailedAssertion", fileName=0xcbd2fe
"partbounds.c", lineNumber=1957)

at assert.c:69
#3 0x0000000000892aa1 in is_dummy_partition (rel=0x19b37c0,

part_index=-1) at partbounds.c:1957

#4 0x00000000008919bd in merge_list_bounds (partnatts=1,

partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0,
inner_rel=0x1922938, jointype=JOIN_INNER,

outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at

partbounds.c:1529

#5 0x00000000008910de in partition_bounds_merge (partnatts=1,

partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0,
inner_rel=0x1922938, jointype=JOIN_INNER,

outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at

partbounds.c:1223

#6 0x000000000082c41a in compute_partition_bounds (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
parent_sjinfo=0x7fffd67752a0, parts1=0x7fffd67751b0,

parts2=0x7fffd67751a8) at joinrels.c:1644
#7 0x000000000082bc34 in try_partitionwise_join (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
parent_sjinfo=0x7fffd67752a0, parent_restrictlist=0x1ab3318)

at joinrels.c:1402
#8 0x000000000082aea2 in populate_joinrel_with_paths (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30, sjinfo=0x7fffd67752a0,
restrictlist=0x1ab3318)

at joinrels.c:926
#9 0x000000000082a8f5 in make_join_rel (root=0x1a19ed0, rel1=0x19b37c0,

rel2=0x1922938) at joinrels.c:760

#10 0x0000000000829e03 in make_rels_by_clause_joins (root=0x1a19ed0,

old_rel=0x19b37c0, other_rels_list=0x1ab2970, other_rels=0x1ab2990) at
joinrels.c:312

#11 0x00000000008298d9 in join_search_one_level (root=0x1a19ed0,

level=2) at joinrels.c:123

#12 0x000000000080c566 in standard_join_search (root=0x1a19ed0,

levels_needed=2, initial_rels=0x1ab2970) at allpaths.c:3020

#13 0x000000000080c4df in make_rel_from_joinlist (root=0x1a19ed0,

joinlist=0x199d538) at allpaths.c:2951

#14 0x000000000080816b in make_one_rel (root=0x1a19ed0,

joinlist=0x199d538) at allpaths.c:228

#15 0x000000000084491d in query_planner (root=0x1a19ed0,

qp_callback=0x84a538 <standard_qp_callback>, qp_extra=0x7fffd6775630) at
planmain.c:276

#16 0x0000000000847040 in grouping_planner (root=0x1a19ed0,

tuple_fraction=0) at planner.c:1447

#17 0x0000000000846709 in subquery_planner (glob=0x19b39d8,

parse=0x1aaa290, parent_root=0x0, hasRecursion=false, tuple_fraction=0) at
planner.c:1025

#18 0x0000000000844f3e in standard_planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at planner.c:406

#19 0x0000000000844ce9 in planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at planner.c:277

#20 0x0000000000978483 in pg_plan_query (querytree=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at postgres.c:847

#21 0x00000000006937fc in ExplainOneQuery (query=0x1aaa290,

cursorOptions=2048, into=0x0, es=0x19b36f0,

queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a,

t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c =
t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

params=0x0, queryEnv=0x0) at explain.c:397
#22 0x0000000000693351 in ExplainQuery (pstate=0x197c410,

stmt=0x1aaa0b0, params=0x0, dest=0x197c378) at explain.c:281

#23 0x00000000009811fa in standard_ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a,

t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c =
t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,

queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at utility.c:845

#24 0x00000000009809ec in ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c, t2.a,

t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c =
t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,

queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at utility.c:527

#25 0x000000000097f636 in PortalRunUtility (portal=0x1893b40,

pstmt=0x1a0bfc8, isTopLevel=true, setHoldSnapshot=true, dest=0x197c378,
qc=0x7fffd6775f90) at pquery.c:1147

#26 0x000000000097f3a5 in FillPortalStore (portal=0x1893b40,

isTopLevel=true) at pquery.c:1026

#27 0x000000000097ed11 in PortalRun (portal=0x1893b40,

count=9223372036854775807, isTopLevel=true, run_once=true, dest=0x1a0c0b8,
altdest=0x1a0c0b8, qc=0x7fffd6776150) at pquery.c:758

#28 0x0000000000978aa5 in exec_simple_query (

Thanks & Regards,
Rajkumar Raghuwanshi

On Fri, Sep 3, 2021 at 7:17 PM Amit Langote <amitlangote09@gmail.com>

wrote:

On Wed, Sep 1, 2021 at 2:31 PM Amit Langote <amitlangote09@gmail.com>

wrote:

On Tue, Aug 31, 2021 at 8:02 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

The attached patch also fixes the above comments.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b, c);
create table p01 partition of p0 for values in ((1, 1, true), (NULL,

1, false));

create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

I guess that may be due to the following newly added code being

incomplete:

+/*
+ * get_partition_bound_null_index
+ *
+ * Returns the partition index of the partition bound which accepts

NULL.

+ */
+int
+get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+{
+   int i = 0;
+   int j = 0;
+
+   if (!boundinfo->isnulls)
+       return -1;
-           if (!val->constisnull)
-               count++;
+   for (i = 0; i < boundinfo->ndatums; i++)
+   {
+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)
+       {
+           if (boundinfo->isnulls[i][j])
+               return boundinfo->indexes[i];
}
}

+ return -1;
+}

Maybe this function needs to return a "bitmapset" of indexes, because
multiple partitions can now contain NULL values.

Some other issues I noticed and suggestions for improvement:

+/*
+ * checkForDuplicates
+ *
+ * Returns TRUE if the list bound element is already present in the

list of

+ * list bounds, FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)

This function name may be too generic. Given that it is specific to
implementing list bound de-duplication, maybe the following signature
is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Also, better if the function comment mentions those parameter names,

like:

"Returns TRUE if the list bound element 'new_bound' is already present
in the target list 'list_bounds', FALSE otherwise."

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw

grammar

+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

+           colname[i] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd statement
overwrites its pointer by the pointer to the string palloc'd by
get_attname().

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate, exprLocation((Node
*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how about
writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Will send more comments in a bit...

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
*/

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+           all_values[j]->values = (Datum *) palloc0(key->partnatts *
sizeof(Datum));
+           all_values[j]->isnulls = (bool *) palloc0(key->partnatts *
sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts *

sizeof(Datum));

Missing a newline between these two statements.

BTW, I noticed that the boundDatums variable is no longer used in
create_list_bounds. I traced back its origin and found that a recent
commit 53d86957e98 introduced it to implement an idea to reduce the
finer-grained pallocs that were being done in create_list_bounds(). I
don't think that this patch needs to throw away that work. You can
make it work as the attached delta patch that applies on top of v3.
Please check.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in partition_bounds_equal()
needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously mentioned.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the above
code stanza which appears to implement a short-cut for this long-form
logic.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

(Skipped looking at merge_list_bounds() and related changes for now as
I see a lot of TODOs remain to be done.)

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the logic
for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the same
goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits the
IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a few
other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

I still have more than half of v3 left to look at, so will continue
looking. In the meantime, please check the changes I suggested,
including the delta patch, and let me know your thoughts.

--
Amit Langote
EDB: http://www.enterprisedb.com

#25Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Rajkumar Raghuwanshi (#24)
1 attachment(s)
Re: Multi-Column List Partitioning

Thanks Rajkumar for testing.

I think it should throw an error as the partition by list has only 1

column but we are giving 2 values.

I also agree that it should throw an error in the above case. Fixed the
issue in the attached patch. Also added related test cases to the
regression test suite.

also if you see \d+ showing plt1_p1 partition value as ‘(0001,0001)’

instead of ('0001','0001').

Now throwing errors in the initial stage, this case doesn't arise.

Please share if you find any other issues.

Thanks & Regards,
Nitin Jadhav

On Thu, Oct 7, 2021 at 4:05 PM Rajkumar Raghuwanshi <
rajkumar.raghuwanshi@enterprisedb.com> wrote:

Show quoted text

Thanks Nitin,

v4 patches applied cleanly and make check is passing now. While testing
further I observed that if multiple values are given for a single
column list partition it is not giving error instead it is changing values
itself. Please find the example below.

postgres=# CREATE TABLE plt1 (a int, b varchar) PARTITION BY LIST(b);
CREATE TABLE
postgres=# CREATE TABLE plt1_p1 PARTITION OF plt1 FOR VALUES IN
(('0001','0001'),('0002','0002'));
CREATE TABLE
postgres=# \d+ plt1;
Partitioned table "public.plt1"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description

--------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
a | integer | | | | plain |
| |
b | character varying | | | | extended |
| |
Partition key: LIST (b)
Partitions: plt1_p1 FOR VALUES IN ('(0001,0001)', '(0002,0002)')

I think it should throw an error as the partition by list has only 1
column but we are giving 2 values.
also if you see \d+ showing plt1_p1 partition value as ‘(0001,0001)’
instead of ('0001','0001').

Thanks & Regards,
Rajkumar Raghuwanshi

On Sun, Oct 3, 2021 at 1:52 AM Nitin Jadhav <nitinjadhavpostgres@gmail.com>
wrote:

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is

failing with below errors.

Thanks Rajkumar for testing.

Here's a v2 of the delta patch that should fix both of these test
failures. As I mentioned in my last reply, my delta patch fixed what
I think were problems in Nitin's v3 patch but were not complete by
themselves. Especially, I hadn't bothered to investigate various /*
TODO: handle multi-column list partitioning */ sites to deal with my
own changes.

Thanks Rajkumar for testing and Thank you Amit for working on v2 of
the delta patch. Actually I had done the code changes related to
partition-wise join and I was in the middle of fixing the review
comments, So I could not share the patch. Anyways thanks for your
efforts.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b, c);
create table p01 partition of p0 for values in ((1, 1, true), (NULL, 1,

false));

create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

In the attached updated version, I've dealt with some of those such
that at least the existing cases exercising partition pruning and
partition wise joins now pass.

wrt partition pruning, I have checked the output of the above case
with the v2 version of the delta patch and without that. The output
remains same. Kindly let me know if I am missing something. But I feel
the above output is correct as the partition p01 is the only partition
which contains NULL value for column a, hence it is showing "Seq scan
on p01" in the output. Kindly correct me if I am wrong. I feel the
code changes related to 'null_keys' is not required, hence not
incorporated that in the attached patch.

wrt partition-wise join, I had run the regression test (with new cases
related to partition-wise join) on v2 of the delta patch and observed
the crash. Hence I have not incorporated the partition-wise join
related code from v2 of delta patch to main v4 patch. Instead I have
added the partition-wise join related code done by me in the attached
patch. Please share your thoughts and if possible we can improvise the
code. Rest of the changes looks good to me and I have incorporated
that in the attached patch.

I guess that may be due to the following newly added code being

incomplete:

Maybe this function needs to return a "bitmapset" of indexes, because
multiple partitions can now contain NULL values.

I feel this function is not required at all as we are not separating
the non null and null partitions now. Removed in the attached patch.
Also removed the "scan_null' variable from the structure
"PruneStepResult" and cleaned up the corresponding code blocks.

This function name may be too generic. Given that it is specific to
implementing list bound de-duplication, maybe the following signature
is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Yes. The function name looks more generic. How about using
"isListBoundDuplicated()"? I have used this name in the patch. Please
let me know if that does not look correct.

Also, better if the function comment mentions those parameter names,

like:

"Returns TRUE if the list bound element 'new_bound' is already present
in the target list 'list_bounds', FALSE otherwise."

Fixed.

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw

grammar

+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

Fixed.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

Fixed in all the places.

+           colname[i] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd statement
overwrites its pointer by the pointer to the string palloc'd by
get_attname().

Removed the 1st statement as it is not required.

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

Fixed in all the places.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate, exprLocation((Node
*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

Fixed.

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how about
writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

This looks good. I have changed in the patch.

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Yes. PartitionListBound looks more appropriate and it also matches the
similar structures of the other partition strategies.

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Retained the old comment.

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
*/

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

Fixed.

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+           all_values[j]->values = (Datum *) palloc0(key->partnatts *
sizeof(Datum));
+           all_values[j]->isnulls = (bool *) palloc0(key->partnatts *
sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts *

sizeof(Datum));

Missing a newline between these two statements.

Fixed. Made necessary changes to keep the intent of existing code.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in partition_bounds_equal()
needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously mentioned.

As mentioned earlier, removed the functionality of
get_partition_bound_null_index(), hence the above condition is not
required and removed.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the above
code stanza which appears to implement a short-cut for this long-form
logic.

Yes. May be we could have ignored the above code stanza if we would
have comparing the null indexes using get_partition_bound_null_index()
in the beginning of the function. But hence we are not separating the
non null partitions and null partitions, I would like to keep the
logic in the inner loop as we are doing it for non null bound values
in the above code stanza, just to give a feel that null bound values
are also handled the same way as non null values. Please correct me if
I am wrong.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

Fixed.

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

Thanks for the suggestion. I have changed as above.

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the logic
for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the same
goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

Actually I had written a separate function because of the complexity.
Now I have understood that since the objective is same, it should be
done in a single function irrespective of complexity.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits the
IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a few
other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

Thank you very much for your efforts. The changes looks good to me and
I have incorporated these changes in the attached patch.

I have completed the coding for all the TODOs and hence removed in the
patch. The naming conventions used for function/variable names varies
across the files. Some places it is like 'namesLikeThis' and in some
place it is like 'names_like_this'. I have used the naming conventions
based on the surrounding styles used. I am happy to change those if
required.

I have verified 'make check' with the attached patch and it is working
fine.

Thanks & Regards,
Nitin Jadhav

On Mon, Sep 13, 2021 at 3:47 PM Rajkumar Raghuwanshi
<rajkumar.raghuwanshi@enterprisedb.com> wrote:

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is

failing with below errors.

--inherit.sql is failing with error :"ERROR: negative bitmapset member

not allowed"

update mlparted_tab mlp set c = 'xxx'
from
(select a from some_tab union all select a+1 from some_tab) ss (a)
where (mlp.a = ss.a and mlp.b = 'b') or mlp.a = 3;
ERROR: negative bitmapset member not allowed

--partition_join.sql is crashing with enable_partitionwise_join set to

true.

CREATE TABLE plt1_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt1_adv_p1 PARTITION OF plt1_adv FOR VALUES IN ('0001',

'0003');

CREATE TABLE plt1_adv_p2 PARTITION OF plt1_adv FOR VALUES IN ('0004',

'0006');

CREATE TABLE plt1_adv_p3 PARTITION OF plt1_adv FOR VALUES IN ('0008',

'0009');

INSERT INTO plt1_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM

generate_series(1, 299) i WHERE i % 10 IN (1, 3, 4, 6, 8, 9);

ANALYZE plt1_adv;
CREATE TABLE plt2_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt2_adv_p1 PARTITION OF plt2_adv FOR VALUES IN ('0002',

'0003');

CREATE TABLE plt2_adv_p2 PARTITION OF plt2_adv FOR VALUES IN ('0004',

'0006');

CREATE TABLE plt2_adv_p3 PARTITION OF plt2_adv FOR VALUES IN ('0007',

'0009');

INSERT INTO plt2_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM

generate_series(1, 299) i WHERE i % 10 IN (2, 3, 4, 6, 7, 9);

ANALYZE plt2_adv;
-- inner join
EXPLAIN (COSTS OFF)
SELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2

ON (t1.a = t2.a AND t1.c = t2.c) WHERE t1.b < 10 ORDER BY t1.a;

server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost

--stack-trace
Core was generated by `postgres: edb regression [local] EXPLAIN

'.

Program terminated with signal 6, Aborted.
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install

glibc-2.17-222.el7.x86_64 keyutils-libs-1.5.8-3.el7.x86_64
krb5-libs-1.15.1-19.el7.x86_64 libcom_err-1.42.9-12.el7_5.x86_64
libgcc-4.8.5-39.el7.x86_64 libselinux-2.5-12.el7.x86_64
openssl-libs-1.0.2k-19.el7.x86_64 pcre-8.32-17.el7.x86_64
zlib-1.2.7-17.el7.x86_64

(gdb) bt
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
#1 0x00007f7d339bb968 in abort () from /lib64/libc.so.6
#2 0x0000000000b0fbc3 in ExceptionalCondition (conditionName=0xcbda10

"part_index >= 0", errorType=0xcbd1c3 "FailedAssertion", fileName=0xcbd2fe
"partbounds.c", lineNumber=1957)

at assert.c:69
#3 0x0000000000892aa1 in is_dummy_partition (rel=0x19b37c0,

part_index=-1) at partbounds.c:1957

#4 0x00000000008919bd in merge_list_bounds (partnatts=1,

partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0,
inner_rel=0x1922938, jointype=JOIN_INNER,

outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at

partbounds.c:1529

#5 0x00000000008910de in partition_bounds_merge (partnatts=1,

partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0,
inner_rel=0x1922938, jointype=JOIN_INNER,

outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at

partbounds.c:1223

#6 0x000000000082c41a in compute_partition_bounds (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
parent_sjinfo=0x7fffd67752a0, parts1=0x7fffd67751b0,

parts2=0x7fffd67751a8) at joinrels.c:1644
#7 0x000000000082bc34 in try_partitionwise_join (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
parent_sjinfo=0x7fffd67752a0, parent_restrictlist=0x1ab3318)

at joinrels.c:1402
#8 0x000000000082aea2 in populate_joinrel_with_paths (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30, sjinfo=0x7fffd67752a0,
restrictlist=0x1ab3318)

at joinrels.c:926
#9 0x000000000082a8f5 in make_join_rel (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938) at joinrels.c:760

#10 0x0000000000829e03 in make_rels_by_clause_joins (root=0x1a19ed0,

old_rel=0x19b37c0, other_rels_list=0x1ab2970, other_rels=0x1ab2990) at
joinrels.c:312

#11 0x00000000008298d9 in join_search_one_level (root=0x1a19ed0,

level=2) at joinrels.c:123

#12 0x000000000080c566 in standard_join_search (root=0x1a19ed0,

levels_needed=2, initial_rels=0x1ab2970) at allpaths.c:3020

#13 0x000000000080c4df in make_rel_from_joinlist (root=0x1a19ed0,

joinlist=0x199d538) at allpaths.c:2951

#14 0x000000000080816b in make_one_rel (root=0x1a19ed0,

joinlist=0x199d538) at allpaths.c:228

#15 0x000000000084491d in query_planner (root=0x1a19ed0,

qp_callback=0x84a538 <standard_qp_callback>, qp_extra=0x7fffd6775630) at
planmain.c:276

#16 0x0000000000847040 in grouping_planner (root=0x1a19ed0,

tuple_fraction=0) at planner.c:1447

#17 0x0000000000846709 in subquery_planner (glob=0x19b39d8,

parse=0x1aaa290, parent_root=0x0, hasRecursion=false, tuple_fraction=0) at
planner.c:1025

#18 0x0000000000844f3e in standard_planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at planner.c:406

#19 0x0000000000844ce9 in planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at planner.c:277

#20 0x0000000000978483 in pg_plan_query (querytree=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at postgres.c:847

#21 0x00000000006937fc in ExplainOneQuery (query=0x1aaa290,

cursorOptions=2048, into=0x0, es=0x19b36f0,

queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

params=0x0, queryEnv=0x0) at explain.c:397
#22 0x0000000000693351 in ExplainQuery (pstate=0x197c410,

stmt=0x1aaa0b0, params=0x0, dest=0x197c378) at explain.c:281

#23 0x00000000009811fa in standard_ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,

queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at utility.c:845

#24 0x00000000009809ec in ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,

queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at utility.c:527

#25 0x000000000097f636 in PortalRunUtility (portal=0x1893b40,

pstmt=0x1a0bfc8, isTopLevel=true, setHoldSnapshot=true, dest=0x197c378,
qc=0x7fffd6775f90) at pquery.c:1147

#26 0x000000000097f3a5 in FillPortalStore (portal=0x1893b40,

isTopLevel=true) at pquery.c:1026

#27 0x000000000097ed11 in PortalRun (portal=0x1893b40,

count=9223372036854775807, isTopLevel=true, run_once=true, dest=0x1a0c0b8,
altdest=0x1a0c0b8, qc=0x7fffd6776150) at pquery.c:758

#28 0x0000000000978aa5 in exec_simple_query (

Thanks & Regards,
Rajkumar Raghuwanshi

On Fri, Sep 3, 2021 at 7:17 PM Amit Langote <amitlangote09@gmail.com>

wrote:

On Wed, Sep 1, 2021 at 2:31 PM Amit Langote <amitlangote09@gmail.com>

wrote:

On Tue, Aug 31, 2021 at 8:02 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

The attached patch also fixes the above comments.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b, c);
create table p01 partition of p0 for values in ((1, 1, true), (NULL,

1, false));

create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

I guess that may be due to the following newly added code being

incomplete:

+/*
+ * get_partition_bound_null_index
+ *
+ * Returns the partition index of the partition bound which accepts

NULL.

+ */
+int
+get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+{
+   int i = 0;
+   int j = 0;
+
+   if (!boundinfo->isnulls)
+       return -1;
-           if (!val->constisnull)
-               count++;
+   for (i = 0; i < boundinfo->ndatums; i++)
+   {
+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)
+       {
+           if (boundinfo->isnulls[i][j])
+               return boundinfo->indexes[i];
}
}

+ return -1;
+}

Maybe this function needs to return a "bitmapset" of indexes, because
multiple partitions can now contain NULL values.

Some other issues I noticed and suggestions for improvement:

+/*
+ * checkForDuplicates
+ *
+ * Returns TRUE if the list bound element is already present in the

list of

+ * list bounds, FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)

This function name may be too generic. Given that it is specific to
implementing list bound de-duplication, maybe the following signature
is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Also, better if the function comment mentions those parameter names,

like:

"Returns TRUE if the list bound element 'new_bound' is already

present

in the target list 'list_bounds', FALSE otherwise."

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw

grammar

+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

+ colname[i] = (char *) palloc0(NAMEDATALEN *

sizeof(char));

+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd

statement

overwrites its pointer by the pointer to the string palloc'd by
get_attname().

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate, exprLocation((Node
*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how about
writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Will send more comments in a bit...

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
*/

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+           all_values[j]->values = (Datum *) palloc0(key->partnatts *
sizeof(Datum));
+           all_values[j]->isnulls = (bool *) palloc0(key->partnatts *
sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts *

sizeof(Datum));

Missing a newline between these two statements.

BTW, I noticed that the boundDatums variable is no longer used in
create_list_bounds. I traced back its origin and found that a recent
commit 53d86957e98 introduced it to implement an idea to reduce the
finer-grained pallocs that were being done in create_list_bounds(). I
don't think that this patch needs to throw away that work. You can
make it work as the attached delta patch that applies on top of v3.
Please check.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in partition_bounds_equal()
needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously mentioned.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the above
code stanza which appears to implement a short-cut for this long-form
logic.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

(Skipped looking at merge_list_bounds() and related changes for now as
I see a lot of TODOs remain to be done.)

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the logic
for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the same
goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits the
IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a few
other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

I still have more than half of v3 left to look at, so will continue
looking. In the meantime, please check the changes I suggested,
including the delta patch, and let me know your thoughts.

--
Amit Langote
EDB: http://www.enterprisedb.com

Attachments:

v5-0001-multi-column-list-partitioning.patchapplication/octet-stream; name=v5-0001-multi-column-list-partitioning.patchDownload
From aa7e1ffff6a42d191b40bfb2986e28f79ff0e502 Mon Sep 17 00:00:00 2001
From: Nitin <nitin.jadhav@enterprisedb.com>
Date: Thu, 7 Oct 2021 17:58:37 +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         |  879 ++++++++++-------
 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, 3361 insertions(+), 545 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ff97b61..35761dc 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..98a800f 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,27 @@ 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)
 			{
+				/* 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 +1471,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 +1485,27 @@ 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)
 			{
+				/* 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 +1520,11 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 													   &default_index);
 				if (merged_index == -1)
 					goto cleanup;
-				merged_datum = inner_datums;
 			}
 
+			merged_datum = outer_datums;
+			merged_isnull = outer_isnull;
+
 			/* Move to the next list value on the inner side. */
 			inner_pos++;
 		}
@@ -1422,29 +1537,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 +1574,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 +1585,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 +1894,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 +2253,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 +2282,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 +2297,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 +2320,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 +2599,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 +2610,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 +2638,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 +2650,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 +3157,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 +3696,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 +3747,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 +3761,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 +3934,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 +4038,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 +4068,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 +4088,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 +4217,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 +4265,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 b15bd81..edd6a85 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

#26Rajkumar Raghuwanshi
rajkumar.raghuwanshi@enterprisedb.com
In reply to: Nitin Jadhav (#25)
Re: Multi-Column List Partitioning

Thanks for the patch, it applied cleanly and fixed the reported issue. I
observed another case where
In case of multi-col list partition on the same column query is not picking
partition wise join. Is this expected?

CREATE TABLE plt1 (a int, b int, c varchar) PARTITION BY LIST(c,c);
CREATE TABLE plt1_p1 PARTITION OF plt1 FOR VALUES IN
(('0001','0001'),('0002','0002'),('0003','0003'));
CREATE TABLE plt1_p2 PARTITION OF plt1 FOR VALUES IN
(('0004','0004'),('0005','0005'),('0006','0006'));
CREATE TABLE plt1_p3 PARTITION OF plt1 DEFAULT;
INSERT INTO plt1 SELECT i, i % 47, to_char(i % 11, 'FM0000') FROM
generate_series(0, 500) i WHERE i % 11 NOT IN (0,10);
ANALYSE plt1;
CREATE TABLE plt2 (a int, b int, c varchar) PARTITION BY LIST(c,c);
CREATE TABLE plt2_p1 PARTITION OF plt2 FOR VALUES IN
(('0001','0001'),('0002','0002'),('0003','0003'));
CREATE TABLE plt2_p2 PARTITION OF plt2 FOR VALUES IN
(('0004','0004'),('0005','0005'),('0006','0006'));
CREATE TABLE plt2_p3 PARTITION OF plt2 DEFAULT;
INSERT INTO plt2 SELECT i, i % 47, to_char(i % 11, 'FM0000') FROM
generate_series(0, 500) i WHERE i % 11 NOT IN (0,10);
ANALYSE plt2;
SET enable_partitionwise_join TO true;
EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.a, t2.c FROM plt1 t1 INNER JOIN
plt2 t2 ON t1.c = t2.c;

postgres=# EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.a, t2.c FROM plt1 t1
INNER JOIN plt2 t2 ON t1.c = t2.c;
QUERY PLAN
--------------------------------------------
Hash Join
Hash Cond: ((t1.c)::text = (t2.c)::text)
-> Append
-> Seq Scan on plt1_p1 t1_1
-> Seq Scan on plt1_p2 t1_2
-> Seq Scan on plt1_p3 t1_3
-> Hash
-> Append
-> Seq Scan on plt2_p1 t2_1
-> Seq Scan on plt2_p2 t2_2
-> Seq Scan on plt2_p3 t2_3
(11 rows)

Thanks & Regards,
Rajkumar Raghuwanshi

On Thu, Oct 7, 2021 at 6:03 PM Nitin Jadhav <nitinjadhavpostgres@gmail.com>
wrote:

Show quoted text

Thanks Rajkumar for testing.

I think it should throw an error as the partition by list has only 1

column but we are giving 2 values.

I also agree that it should throw an error in the above case. Fixed the
issue in the attached patch. Also added related test cases to the
regression test suite.

also if you see \d+ showing plt1_p1 partition value as ‘(0001,0001)’

instead of ('0001','0001').

Now throwing errors in the initial stage, this case doesn't arise.

Please share if you find any other issues.

Thanks & Regards,
Nitin Jadhav

On Thu, Oct 7, 2021 at 4:05 PM Rajkumar Raghuwanshi <
rajkumar.raghuwanshi@enterprisedb.com> wrote:

Thanks Nitin,

v4 patches applied cleanly and make check is passing now. While testing
further I observed that if multiple values are given for a single
column list partition it is not giving error instead it is changing
values itself. Please find the example below.

postgres=# CREATE TABLE plt1 (a int, b varchar) PARTITION BY LIST(b);
CREATE TABLE
postgres=# CREATE TABLE plt1_p1 PARTITION OF plt1 FOR VALUES IN
(('0001','0001'),('0002','0002'));
CREATE TABLE
postgres=# \d+ plt1;
Partitioned table "public.plt1"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description

--------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
a | integer | | | | plain |
| |
b | character varying | | | | extended |
| |
Partition key: LIST (b)
Partitions: plt1_p1 FOR VALUES IN ('(0001,0001)', '(0002,0002)')

I think it should throw an error as the partition by list has only 1
column but we are giving 2 values.
also if you see \d+ showing plt1_p1 partition value as ‘(0001,0001)’
instead of ('0001','0001').

Thanks & Regards,
Rajkumar Raghuwanshi

On Sun, Oct 3, 2021 at 1:52 AM Nitin Jadhav <
nitinjadhavpostgres@gmail.com> wrote:

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is

failing with below errors.

Thanks Rajkumar for testing.

Here's a v2 of the delta patch that should fix both of these test
failures. As I mentioned in my last reply, my delta patch fixed what
I think were problems in Nitin's v3 patch but were not complete by
themselves. Especially, I hadn't bothered to investigate various /*
TODO: handle multi-column list partitioning */ sites to deal with my
own changes.

Thanks Rajkumar for testing and Thank you Amit for working on v2 of
the delta patch. Actually I had done the code changes related to
partition-wise join and I was in the middle of fixing the review
comments, So I could not share the patch. Anyways thanks for your
efforts.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b, c);
create table p01 partition of p0 for values in ((1, 1, true), (NULL,

1, false));

create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

In the attached updated version, I've dealt with some of those such
that at least the existing cases exercising partition pruning and
partition wise joins now pass.

wrt partition pruning, I have checked the output of the above case
with the v2 version of the delta patch and without that. The output
remains same. Kindly let me know if I am missing something. But I feel
the above output is correct as the partition p01 is the only partition
which contains NULL value for column a, hence it is showing "Seq scan
on p01" in the output. Kindly correct me if I am wrong. I feel the
code changes related to 'null_keys' is not required, hence not
incorporated that in the attached patch.

wrt partition-wise join, I had run the regression test (with new cases
related to partition-wise join) on v2 of the delta patch and observed
the crash. Hence I have not incorporated the partition-wise join
related code from v2 of delta patch to main v4 patch. Instead I have
added the partition-wise join related code done by me in the attached
patch. Please share your thoughts and if possible we can improvise the
code. Rest of the changes looks good to me and I have incorporated
that in the attached patch.

I guess that may be due to the following newly added code being

incomplete:

Maybe this function needs to return a "bitmapset" of indexes, because
multiple partitions can now contain NULL values.

I feel this function is not required at all as we are not separating
the non null and null partitions now. Removed in the attached patch.
Also removed the "scan_null' variable from the structure
"PruneStepResult" and cleaned up the corresponding code blocks.

This function name may be too generic. Given that it is specific to
implementing list bound de-duplication, maybe the following signature
is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Yes. The function name looks more generic. How about using
"isListBoundDuplicated()"? I have used this name in the patch. Please
let me know if that does not look correct.

Also, better if the function comment mentions those parameter names,

like:

"Returns TRUE if the list bound element 'new_bound' is already present
in the target list 'list_bounds', FALSE otherwise."

Fixed.

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw

grammar

+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

Fixed.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

Fixed in all the places.

+           colname[i] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd statement
overwrites its pointer by the pointer to the string palloc'd by
get_attname().

Removed the 1st statement as it is not required.

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

Fixed in all the places.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate, exprLocation((Node
*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

Fixed.

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how about
writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

This looks good. I have changed in the patch.

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Yes. PartitionListBound looks more appropriate and it also matches the
similar structures of the other partition strategies.

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Retained the old comment.

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
*/

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

Fixed.

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+           all_values[j]->values = (Datum *) palloc0(key->partnatts *
sizeof(Datum));
+           all_values[j]->isnulls = (bool *) palloc0(key->partnatts *
sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts *

sizeof(Datum));

Missing a newline between these two statements.

Fixed. Made necessary changes to keep the intent of existing code.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in partition_bounds_equal()
needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously mentioned.

As mentioned earlier, removed the functionality of
get_partition_bound_null_index(), hence the above condition is not
required and removed.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the above
code stanza which appears to implement a short-cut for this long-form
logic.

Yes. May be we could have ignored the above code stanza if we would
have comparing the null indexes using get_partition_bound_null_index()
in the beginning of the function. But hence we are not separating the
non null partitions and null partitions, I would like to keep the
logic in the inner loop as we are doing it for non null bound values
in the above code stanza, just to give a feel that null bound values
are also handled the same way as non null values. Please correct me if
I am wrong.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

Fixed.

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

Thanks for the suggestion. I have changed as above.

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the logic
for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the same
goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

Actually I had written a separate function because of the complexity.
Now I have understood that since the objective is same, it should be
done in a single function irrespective of complexity.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits the
IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a few
other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

Thank you very much for your efforts. The changes looks good to me and
I have incorporated these changes in the attached patch.

I have completed the coding for all the TODOs and hence removed in the
patch. The naming conventions used for function/variable names varies
across the files. Some places it is like 'namesLikeThis' and in some
place it is like 'names_like_this'. I have used the naming conventions
based on the surrounding styles used. I am happy to change those if
required.

I have verified 'make check' with the attached patch and it is working
fine.

Thanks & Regards,
Nitin Jadhav

On Mon, Sep 13, 2021 at 3:47 PM Rajkumar Raghuwanshi
<rajkumar.raghuwanshi@enterprisedb.com> wrote:

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is

failing with below errors.

--inherit.sql is failing with error :"ERROR: negative bitmapset

member not allowed"

update mlparted_tab mlp set c = 'xxx'
from
(select a from some_tab union all select a+1 from some_tab) ss (a)
where (mlp.a = ss.a and mlp.b = 'b') or mlp.a = 3;
ERROR: negative bitmapset member not allowed

--partition_join.sql is crashing with enable_partitionwise_join set to

true.

CREATE TABLE plt1_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt1_adv_p1 PARTITION OF plt1_adv FOR VALUES IN ('0001',

'0003');

CREATE TABLE plt1_adv_p2 PARTITION OF plt1_adv FOR VALUES IN ('0004',

'0006');

CREATE TABLE plt1_adv_p3 PARTITION OF plt1_adv FOR VALUES IN ('0008',

'0009');

INSERT INTO plt1_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM

generate_series(1, 299) i WHERE i % 10 IN (1, 3, 4, 6, 8, 9);

ANALYZE plt1_adv;
CREATE TABLE plt2_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt2_adv_p1 PARTITION OF plt2_adv FOR VALUES IN ('0002',

'0003');

CREATE TABLE plt2_adv_p2 PARTITION OF plt2_adv FOR VALUES IN ('0004',

'0006');

CREATE TABLE plt2_adv_p3 PARTITION OF plt2_adv FOR VALUES IN ('0007',

'0009');

INSERT INTO plt2_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM

generate_series(1, 299) i WHERE i % 10 IN (2, 3, 4, 6, 7, 9);

ANALYZE plt2_adv;
-- inner join
EXPLAIN (COSTS OFF)
SELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2

ON (t1.a = t2.a AND t1.c = t2.c) WHERE t1.b < 10 ORDER BY t1.a;

server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost

--stack-trace
Core was generated by `postgres: edb regression [local] EXPLAIN

'.

Program terminated with signal 6, Aborted.
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install

glibc-2.17-222.el7.x86_64 keyutils-libs-1.5.8-3.el7.x86_64
krb5-libs-1.15.1-19.el7.x86_64 libcom_err-1.42.9-12.el7_5.x86_64
libgcc-4.8.5-39.el7.x86_64 libselinux-2.5-12.el7.x86_64
openssl-libs-1.0.2k-19.el7.x86_64 pcre-8.32-17.el7.x86_64
zlib-1.2.7-17.el7.x86_64

(gdb) bt
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
#1 0x00007f7d339bb968 in abort () from /lib64/libc.so.6
#2 0x0000000000b0fbc3 in ExceptionalCondition (conditionName=0xcbda10

"part_index >= 0", errorType=0xcbd1c3 "FailedAssertion", fileName=0xcbd2fe
"partbounds.c", lineNumber=1957)

at assert.c:69
#3 0x0000000000892aa1 in is_dummy_partition (rel=0x19b37c0,

part_index=-1) at partbounds.c:1957

#4 0x00000000008919bd in merge_list_bounds (partnatts=1,

partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0,
inner_rel=0x1922938, jointype=JOIN_INNER,

outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at

partbounds.c:1529

#5 0x00000000008910de in partition_bounds_merge (partnatts=1,

partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0,
inner_rel=0x1922938, jointype=JOIN_INNER,

outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at

partbounds.c:1223

#6 0x000000000082c41a in compute_partition_bounds (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
parent_sjinfo=0x7fffd67752a0, parts1=0x7fffd67751b0,

parts2=0x7fffd67751a8) at joinrels.c:1644
#7 0x000000000082bc34 in try_partitionwise_join (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
parent_sjinfo=0x7fffd67752a0, parent_restrictlist=0x1ab3318)

at joinrels.c:1402
#8 0x000000000082aea2 in populate_joinrel_with_paths (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30, sjinfo=0x7fffd67752a0,
restrictlist=0x1ab3318)

at joinrels.c:926
#9 0x000000000082a8f5 in make_join_rel (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938) at joinrels.c:760

#10 0x0000000000829e03 in make_rels_by_clause_joins (root=0x1a19ed0,

old_rel=0x19b37c0, other_rels_list=0x1ab2970, other_rels=0x1ab2990) at
joinrels.c:312

#11 0x00000000008298d9 in join_search_one_level (root=0x1a19ed0,

level=2) at joinrels.c:123

#12 0x000000000080c566 in standard_join_search (root=0x1a19ed0,

levels_needed=2, initial_rels=0x1ab2970) at allpaths.c:3020

#13 0x000000000080c4df in make_rel_from_joinlist (root=0x1a19ed0,

joinlist=0x199d538) at allpaths.c:2951

#14 0x000000000080816b in make_one_rel (root=0x1a19ed0,

joinlist=0x199d538) at allpaths.c:228

#15 0x000000000084491d in query_planner (root=0x1a19ed0,

qp_callback=0x84a538 <standard_qp_callback>, qp_extra=0x7fffd6775630) at
planmain.c:276

#16 0x0000000000847040 in grouping_planner (root=0x1a19ed0,

tuple_fraction=0) at planner.c:1447

#17 0x0000000000846709 in subquery_planner (glob=0x19b39d8,

parse=0x1aaa290, parent_root=0x0, hasRecursion=false, tuple_fraction=0) at
planner.c:1025

#18 0x0000000000844f3e in standard_planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at planner.c:406

#19 0x0000000000844ce9 in planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at planner.c:277

#20 0x0000000000978483 in pg_plan_query (querytree=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at postgres.c:847

#21 0x00000000006937fc in ExplainOneQuery (query=0x1aaa290,

cursorOptions=2048, into=0x0, es=0x19b36f0,

queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

params=0x0, queryEnv=0x0) at explain.c:397
#22 0x0000000000693351 in ExplainQuery (pstate=0x197c410,

stmt=0x1aaa0b0, params=0x0, dest=0x197c378) at explain.c:281

#23 0x00000000009811fa in standard_ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,

queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at utility.c:845

#24 0x00000000009809ec in ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,

queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at utility.c:527

#25 0x000000000097f636 in PortalRunUtility (portal=0x1893b40,

pstmt=0x1a0bfc8, isTopLevel=true, setHoldSnapshot=true, dest=0x197c378,
qc=0x7fffd6775f90) at pquery.c:1147

#26 0x000000000097f3a5 in FillPortalStore (portal=0x1893b40,

isTopLevel=true) at pquery.c:1026

#27 0x000000000097ed11 in PortalRun (portal=0x1893b40,

count=9223372036854775807, isTopLevel=true, run_once=true, dest=0x1a0c0b8,
altdest=0x1a0c0b8, qc=0x7fffd6776150) at pquery.c:758

#28 0x0000000000978aa5 in exec_simple_query (

Thanks & Regards,
Rajkumar Raghuwanshi

On Fri, Sep 3, 2021 at 7:17 PM Amit Langote <amitlangote09@gmail.com>

wrote:

On Wed, Sep 1, 2021 at 2:31 PM Amit Langote <amitlangote09@gmail.com>

wrote:

On Tue, Aug 31, 2021 at 8:02 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

The attached patch also fixes the above comments.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b, c);
create table p01 partition of p0 for values in ((1, 1, true),

(NULL, 1, false));

create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

I guess that may be due to the following newly added code being

incomplete:

+/*
+ * get_partition_bound_null_index
+ *
+ * Returns the partition index of the partition bound which

accepts NULL.

+ */
+int
+get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+{
+   int i = 0;
+   int j = 0;
+
+   if (!boundinfo->isnulls)
+       return -1;
-           if (!val->constisnull)
-               count++;
+   for (i = 0; i < boundinfo->ndatums; i++)
+   {
+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)
+       {
+           if (boundinfo->isnulls[i][j])
+               return boundinfo->indexes[i];
}
}

+ return -1;
+}

Maybe this function needs to return a "bitmapset" of indexes,

because

multiple partitions can now contain NULL values.

Some other issues I noticed and suggestions for improvement:

+/*
+ * checkForDuplicates
+ *
+ * Returns TRUE if the list bound element is already present in

the list of

+ * list bounds, FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)

This function name may be too generic. Given that it is specific to
implementing list bound de-duplication, maybe the following

signature

is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Also, better if the function comment mentions those parameter

names, like:

"Returns TRUE if the list bound element 'new_bound' is already

present

in the target list 'list_bounds', FALSE otherwise."

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw

grammar

+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

+ colname[i] = (char *) palloc0(NAMEDATALEN *

sizeof(char));

+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd

statement

overwrites its pointer by the pointer to the string palloc'd by
get_attname().

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate,

exprLocation((Node

*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how

about

writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Will send more comments in a bit...

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
*/

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+           all_values[j]->values = (Datum *) palloc0(key->partnatts *
sizeof(Datum));
+           all_values[j]->isnulls = (bool *) palloc0(key->partnatts *
sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts *

sizeof(Datum));

Missing a newline between these two statements.

BTW, I noticed that the boundDatums variable is no longer used in
create_list_bounds. I traced back its origin and found that a recent
commit 53d86957e98 introduced it to implement an idea to reduce the
finer-grained pallocs that were being done in create_list_bounds(). I
don't think that this patch needs to throw away that work. You can
make it work as the attached delta patch that applies on top of v3.
Please check.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in partition_bounds_equal()
needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously mentioned.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the above
code stanza which appears to implement a short-cut for this long-form
logic.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

(Skipped looking at merge_list_bounds() and related changes for now as
I see a lot of TODOs remain to be done.)

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the logic
for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the same
goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits the
IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a few
other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

I still have more than half of v3 left to look at, so will continue
looking. In the meantime, please check the changes I suggested,
including the delta patch, and let me know your thoughts.

--
Amit Langote
EDB: http://www.enterprisedb.com

#27Rajkumar Raghuwanshi
rajkumar.raghuwanshi@enterprisedb.com
In reply to: Rajkumar Raghuwanshi (#26)
Re: Multi-Column List Partitioning

Hi Nitin,

While testing further I got a crash with partition wise join enabled for
multi-col list partitions. please find test case & stack-trace below.

SET enable_partitionwise_join TO on;
CREATE TABLE plt1 (c varchar, d varchar) PARTITION BY LIST(c,d);
CREATE TABLE plt1_p1 PARTITION OF plt1 FOR VALUES IN
(('0001','0001'),('0002','0002'),(NULL,NULL));
CREATE TABLE plt1_p2 PARTITION OF plt1 FOR VALUES IN
(('0004','0004'),('0005','0005'),('0006','0006'));
INSERT INTO plt1 SELECT to_char(i % 11, 'FM0000'), to_char(i % 11,
'FM0000') FROM generate_series(0, 500) i WHERE i % 11 NOT IN (0,10,3,7,8,9);
INSERT INTO plt1 SELECT NULL,NULL FROM generate_series(0, 500) i WHERE i %
11 IN (3);
ANALYSE plt1;
CREATE TABLE plt2 (c varchar, d varchar) PARTITION BY LIST(c,d);
CREATE TABLE plt2_p1 PARTITION OF plt2 FOR VALUES IN
(('0001','0001'),('0002','0002'));
CREATE TABLE plt2_p2 PARTITION OF plt2 FOR VALUES IN
(('0004','0004'),('0005','0005'),('0006','0006'));
CREATE TABLE plt2_p3 PARTITION OF plt2 DEFAULT;
INSERT INTO plt2 SELECT to_char(i % 11, 'FM0000'), to_char(i % 11,
'FM0000') FROM generate_series(0, 500) i WHERE i % 11 NOT IN (0,10,3);
INSERT INTO plt2 SELECT NULL,NULL FROM generate_series(0, 500) i WHERE i %
11 IN (3);
ANALYSE plt2;

EXPLAIN (COSTS OFF)
SELECT t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON
(t1.c = t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d =
t3.d);

postgres=# EXPLAIN (COSTS OFF)
postgres-# SELECT t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN
plt2 t2 ON (t1.c = t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c
AND t2.d = t3.d);
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
The connection to the server was lost. Attempting reset: Failed.
!?> \q
[edb@localhost bin]$ gdb -q -c data/core.66926 postgres
Reading symbols from
/home/edb/WORK/pg_src/PG_TEMP/postgresql/inst/bin/postgres...done.
[New LWP 66926]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Core was generated by `postgres: edb postgres [local] EXPLAIN
'.
Program terminated with signal 11, Segmentation fault.
#0 0x000000000082be39 in is_dummy_rel (rel=0x40) at joinrels.c:1221
1221 if (rel->pathlist == NIL)
(gdb) bt
#0 0x000000000082be39 in is_dummy_rel (rel=0x40) at joinrels.c:1221
#1 0x000000000089341c in is_dummy_partition (rel=0x2f86e88, part_index=2)
at partbounds.c:1959
#2 0x0000000000891d38 in merge_list_bounds (partnatts=2,
partsupfunc=0x2f70058, partcollation=0x2fd3c98, outer_rel=0x2f86e88,
inner_rel=0x2fd4368, jointype=JOIN_LEFT,
outer_parts=0x7ffea91f8cc0, inner_parts=0x7ffea91f8cb8) at
partbounds.c:1325
#3 0x0000000000891991 in partition_bounds_merge (partnatts=2,
partsupfunc=0x2f70058, partcollation=0x2fd3c98, outer_rel=0x2f86e88,
inner_rel=0x2fd4368, jointype=JOIN_LEFT,
outer_parts=0x7ffea91f8cc0, inner_parts=0x7ffea91f8cb8) at
partbounds.c:1198
#4 0x000000000082cc5a in compute_partition_bounds (root=0x2f9e910,
rel1=0x2f86e88, rel2=0x2fd4368, joinrel=0x2fae388, parent_sjinfo=0x2f7dfa8,
parts1=0x7ffea91f8cc0,
parts2=0x7ffea91f8cb8) at joinrels.c:1644
#5 0x000000000082c474 in try_partitionwise_join (root=0x2f9e910,
rel1=0x2f86e88, rel2=0x2fd4368, joinrel=0x2fae388, parent_sjinfo=0x2f7dfa8,
parent_restrictlist=0x2fae650)
at joinrels.c:1402
#6 0x000000000082b6e2 in populate_joinrel_with_paths (root=0x2f9e910,
rel1=0x2f86e88, rel2=0x2fd4368, joinrel=0x2fae388, sjinfo=0x2f7dfa8,
restrictlist=0x2fae650) at joinrels.c:926
#7 0x000000000082b135 in make_join_rel (root=0x2f9e910, rel1=0x2f86e88,
rel2=0x2fd4368) at joinrels.c:760
#8 0x000000000082a643 in make_rels_by_clause_joins (root=0x2f9e910,
old_rel=0x2f86e88, other_rels_list=0x2f90148, other_rels=0x2f90160) at
joinrels.c:312
#9 0x000000000082a119 in join_search_one_level (root=0x2f9e910, level=3)
at joinrels.c:123
#10 0x000000000080cd97 in standard_join_search (root=0x2f9e910,
levels_needed=3, initial_rels=0x2f90148) at allpaths.c:3020
#11 0x000000000080cd10 in make_rel_from_joinlist (root=0x2f9e910,
joinlist=0x2fd7550) at allpaths.c:2951
#12 0x000000000080899a in make_one_rel (root=0x2f9e910, joinlist=0x2fd7550)
at allpaths.c:228
#13 0x000000000084516a in query_planner (root=0x2f9e910,
qp_callback=0x84ad85 <standard_qp_callback>, qp_extra=0x7ffea91f9140) at
planmain.c:276
#14 0x000000000084788d in grouping_planner (root=0x2f9e910,
tuple_fraction=0) at planner.c:1447
#15 0x0000000000846f56 in subquery_planner (glob=0x2fa0c08,
parse=0x2f56d30, parent_root=0x0, hasRecursion=false, tuple_fraction=0) at
planner.c:1025
#16 0x000000000084578b in standard_planner (parse=0x2f56d30,
query_string=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);",
cursorOptions=2048, boundParams=0x0) at planner.c:406
#17 0x0000000000845536 in planner (parse=0x2f56d30,
query_string=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);",
cursorOptions=2048, boundParams=0x0) at planner.c:277
#18 0x0000000000978faf in pg_plan_query (querytree=0x2f56d30,
query_string=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);",
cursorOptions=2048, boundParams=0x0) at postgres.c:847
#19 0x0000000000693e50 in ExplainOneQuery (query=0x2f56d30,
cursorOptions=2048, into=0x0, es=0x2fa0920,
queryString=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);",
params=0x0, queryEnv=0x0) at explain.c:397
#20 0x00000000006939a5 in ExplainQuery (pstate=0x2f9e0a0, stmt=0x2f56b50,
params=0x0, dest=0x2f9e008) at explain.c:281
#21 0x0000000000981de8 in standard_ProcessUtility (pstmt=0x2fd2220,
queryString=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);",
readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,
queryEnv=0x0, dest=0x2f9e008, qc=0x7ffea91f9aa0) at utility.c:862
#22 0x0000000000981585 in ProcessUtility (pstmt=0x2fd2220,
queryString=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);",
readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,
queryEnv=0x0, dest=0x2f9e008, qc=0x7ffea91f9aa0) at utility.c:527
#23 0x00000000009801ba in PortalRunUtility (portal=0x2f10180,
pstmt=0x2fd2220, isTopLevel=true, setHoldSnapshot=true, dest=0x2f9e008,
qc=0x7ffea91f9aa0) at pquery.c:1155
#24 0x000000000097ff20 in FillPortalStore (portal=0x2f10180,
isTopLevel=true) at pquery.c:1028
#25 0x000000000097f883 in PortalRun (portal=0x2f10180,
count=9223372036854775807, isTopLevel=true, run_once=true, dest=0x2fd2310,
altdest=0x2fd2310, qc=0x7ffea91f9c60) at pquery.c:760
#26 0x00000000009795d1 in exec_simple_query (
query_string=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);")
at postgres.c:1214
#27 0x000000000097da8d in PostgresMain (dbname=0x2ed8068 "postgres",
username=0x2ed8048 "edb") at postgres.c:4497
#28 0x00000000008b9699 in BackendRun (port=0x2ecfd00) at postmaster.c:4560

Thanks & Regards,
Rajkumar Raghuwanshi

On Mon, Oct 11, 2021 at 11:05 AM Rajkumar Raghuwanshi <
rajkumar.raghuwanshi@enterprisedb.com> wrote:

Show quoted text

Thanks for the patch, it applied cleanly and fixed the reported issue. I
observed another case where
In case of multi-col list partition on the same column query is not
picking partition wise join. Is this expected?

CREATE TABLE plt1 (a int, b int, c varchar) PARTITION BY LIST(c,c);
CREATE TABLE plt1_p1 PARTITION OF plt1 FOR VALUES IN
(('0001','0001'),('0002','0002'),('0003','0003'));
CREATE TABLE plt1_p2 PARTITION OF plt1 FOR VALUES IN
(('0004','0004'),('0005','0005'),('0006','0006'));
CREATE TABLE plt1_p3 PARTITION OF plt1 DEFAULT;
INSERT INTO plt1 SELECT i, i % 47, to_char(i % 11, 'FM0000') FROM
generate_series(0, 500) i WHERE i % 11 NOT IN (0,10);
ANALYSE plt1;
CREATE TABLE plt2 (a int, b int, c varchar) PARTITION BY LIST(c,c);
CREATE TABLE plt2_p1 PARTITION OF plt2 FOR VALUES IN
(('0001','0001'),('0002','0002'),('0003','0003'));
CREATE TABLE plt2_p2 PARTITION OF plt2 FOR VALUES IN
(('0004','0004'),('0005','0005'),('0006','0006'));
CREATE TABLE plt2_p3 PARTITION OF plt2 DEFAULT;
INSERT INTO plt2 SELECT i, i % 47, to_char(i % 11, 'FM0000') FROM
generate_series(0, 500) i WHERE i % 11 NOT IN (0,10);
ANALYSE plt2;
SET enable_partitionwise_join TO true;
EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.a, t2.c FROM plt1 t1 INNER JOIN
plt2 t2 ON t1.c = t2.c;

postgres=# EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.a, t2.c FROM plt1 t1
INNER JOIN plt2 t2 ON t1.c = t2.c;
QUERY PLAN
--------------------------------------------
Hash Join
Hash Cond: ((t1.c)::text = (t2.c)::text)
-> Append
-> Seq Scan on plt1_p1 t1_1
-> Seq Scan on plt1_p2 t1_2
-> Seq Scan on plt1_p3 t1_3
-> Hash
-> Append
-> Seq Scan on plt2_p1 t2_1
-> Seq Scan on plt2_p2 t2_2
-> Seq Scan on plt2_p3 t2_3
(11 rows)

Thanks & Regards,
Rajkumar Raghuwanshi

On Thu, Oct 7, 2021 at 6:03 PM Nitin Jadhav <nitinjadhavpostgres@gmail.com>
wrote:

Thanks Rajkumar for testing.

I think it should throw an error as the partition by list has only 1

column but we are giving 2 values.

I also agree that it should throw an error in the above case. Fixed the
issue in the attached patch. Also added related test cases to the
regression test suite.

also if you see \d+ showing plt1_p1 partition value as ‘(0001,0001)’

instead of ('0001','0001').

Now throwing errors in the initial stage, this case doesn't arise.

Please share if you find any other issues.

Thanks & Regards,
Nitin Jadhav

On Thu, Oct 7, 2021 at 4:05 PM Rajkumar Raghuwanshi <
rajkumar.raghuwanshi@enterprisedb.com> wrote:

Thanks Nitin,

v4 patches applied cleanly and make check is passing now. While testing
further I observed that if multiple values are given for a single
column list partition it is not giving error instead it is changing
values itself. Please find the example below.

postgres=# CREATE TABLE plt1 (a int, b varchar) PARTITION BY LIST(b);
CREATE TABLE
postgres=# CREATE TABLE plt1_p1 PARTITION OF plt1 FOR VALUES IN
(('0001','0001'),('0002','0002'));
CREATE TABLE
postgres=# \d+ plt1;
Partitioned table "public.plt1"
Column | Type | Collation | Nullable | Default | Storage
| Compression | Stats target | Description

--------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
a | integer | | | | plain
| | |
b | character varying | | | | extended
| | |
Partition key: LIST (b)
Partitions: plt1_p1 FOR VALUES IN ('(0001,0001)', '(0002,0002)')

I think it should throw an error as the partition by list has only 1
column but we are giving 2 values.
also if you see \d+ showing plt1_p1 partition value as ‘(0001,0001)’
instead of ('0001','0001').

Thanks & Regards,
Rajkumar Raghuwanshi

On Sun, Oct 3, 2021 at 1:52 AM Nitin Jadhav <
nitinjadhavpostgres@gmail.com> wrote:

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is

failing with below errors.

Thanks Rajkumar for testing.

Here's a v2 of the delta patch that should fix both of these test
failures. As I mentioned in my last reply, my delta patch fixed what
I think were problems in Nitin's v3 patch but were not complete by
themselves. Especially, I hadn't bothered to investigate various /*
TODO: handle multi-column list partitioning */ sites to deal with my
own changes.

Thanks Rajkumar for testing and Thank you Amit for working on v2 of
the delta patch. Actually I had done the code changes related to
partition-wise join and I was in the middle of fixing the review
comments, So I could not share the patch. Anyways thanks for your
efforts.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b, c);
create table p01 partition of p0 for values in ((1, 1, true), (NULL,

1, false));

create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

In the attached updated version, I've dealt with some of those such
that at least the existing cases exercising partition pruning and
partition wise joins now pass.

wrt partition pruning, I have checked the output of the above case
with the v2 version of the delta patch and without that. The output
remains same. Kindly let me know if I am missing something. But I feel
the above output is correct as the partition p01 is the only partition
which contains NULL value for column a, hence it is showing "Seq scan
on p01" in the output. Kindly correct me if I am wrong. I feel the
code changes related to 'null_keys' is not required, hence not
incorporated that in the attached patch.

wrt partition-wise join, I had run the regression test (with new cases
related to partition-wise join) on v2 of the delta patch and observed
the crash. Hence I have not incorporated the partition-wise join
related code from v2 of delta patch to main v4 patch. Instead I have
added the partition-wise join related code done by me in the attached
patch. Please share your thoughts and if possible we can improvise the
code. Rest of the changes looks good to me and I have incorporated
that in the attached patch.

I guess that may be due to the following newly added code being

incomplete:

Maybe this function needs to return a "bitmapset" of indexes, because
multiple partitions can now contain NULL values.

I feel this function is not required at all as we are not separating
the non null and null partitions now. Removed in the attached patch.
Also removed the "scan_null' variable from the structure
"PruneStepResult" and cleaned up the corresponding code blocks.

This function name may be too generic. Given that it is specific to
implementing list bound de-duplication, maybe the following signature
is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Yes. The function name looks more generic. How about using
"isListBoundDuplicated()"? I have used this name in the patch. Please
let me know if that does not look correct.

Also, better if the function comment mentions those parameter names,

like:

"Returns TRUE if the list bound element 'new_bound' is already present
in the target list 'list_bounds', FALSE otherwise."

Fixed.

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw

grammar

+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

Fixed.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

Fixed in all the places.

+           colname[i] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd statement
overwrites its pointer by the pointer to the string palloc'd by
get_attname().

Removed the 1st statement as it is not required.

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

Fixed in all the places.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate, exprLocation((Node
*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

Fixed.

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how about
writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

This looks good. I have changed in the patch.

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Yes. PartitionListBound looks more appropriate and it also matches the
similar structures of the other partition strategies.

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Retained the old comment.

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
*/

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

Fixed.

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+           all_values[j]->values = (Datum *) palloc0(key->partnatts *
sizeof(Datum));
+           all_values[j]->isnulls = (bool *) palloc0(key->partnatts *
sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts *

sizeof(Datum));

Missing a newline between these two statements.

Fixed. Made necessary changes to keep the intent of existing code.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in partition_bounds_equal()
needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously mentioned.

As mentioned earlier, removed the functionality of
get_partition_bound_null_index(), hence the above condition is not
required and removed.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the above
code stanza which appears to implement a short-cut for this long-form
logic.

Yes. May be we could have ignored the above code stanza if we would
have comparing the null indexes using get_partition_bound_null_index()
in the beginning of the function. But hence we are not separating the
non null partitions and null partitions, I would like to keep the
logic in the inner loop as we are doing it for non null bound values
in the above code stanza, just to give a feel that null bound values
are also handled the same way as non null values. Please correct me if
I am wrong.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

Fixed.

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

Thanks for the suggestion. I have changed as above.

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the logic
for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the same
goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

Actually I had written a separate function because of the complexity.
Now I have understood that since the objective is same, it should be
done in a single function irrespective of complexity.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits the
IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a few
other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

Thank you very much for your efforts. The changes looks good to me and
I have incorporated these changes in the attached patch.

I have completed the coding for all the TODOs and hence removed in the
patch. The naming conventions used for function/variable names varies
across the files. Some places it is like 'namesLikeThis' and in some
place it is like 'names_like_this'. I have used the naming conventions
based on the surrounding styles used. I am happy to change those if
required.

I have verified 'make check' with the attached patch and it is working
fine.

Thanks & Regards,
Nitin Jadhav

On Mon, Sep 13, 2021 at 3:47 PM Rajkumar Raghuwanshi
<rajkumar.raghuwanshi@enterprisedb.com> wrote:

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is

failing with below errors.

--inherit.sql is failing with error :"ERROR: negative bitmapset

member not allowed"

update mlparted_tab mlp set c = 'xxx'
from
(select a from some_tab union all select a+1 from some_tab) ss (a)
where (mlp.a = ss.a and mlp.b = 'b') or mlp.a = 3;
ERROR: negative bitmapset member not allowed

--partition_join.sql is crashing with enable_partitionwise_join set

to true.

CREATE TABLE plt1_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt1_adv_p1 PARTITION OF plt1_adv FOR VALUES IN ('0001',

'0003');

CREATE TABLE plt1_adv_p2 PARTITION OF plt1_adv FOR VALUES IN ('0004',

'0006');

CREATE TABLE plt1_adv_p3 PARTITION OF plt1_adv FOR VALUES IN ('0008',

'0009');

INSERT INTO plt1_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM

generate_series(1, 299) i WHERE i % 10 IN (1, 3, 4, 6, 8, 9);

ANALYZE plt1_adv;
CREATE TABLE plt2_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt2_adv_p1 PARTITION OF plt2_adv FOR VALUES IN ('0002',

'0003');

CREATE TABLE plt2_adv_p2 PARTITION OF plt2_adv FOR VALUES IN ('0004',

'0006');

CREATE TABLE plt2_adv_p3 PARTITION OF plt2_adv FOR VALUES IN ('0007',

'0009');

INSERT INTO plt2_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM

generate_series(1, 299) i WHERE i % 10 IN (2, 3, 4, 6, 7, 9);

ANALYZE plt2_adv;
-- inner join
EXPLAIN (COSTS OFF)
SELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2

ON (t1.a = t2.a AND t1.c = t2.c) WHERE t1.b < 10 ORDER BY t1.a;

server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost

--stack-trace
Core was generated by `postgres: edb regression [local] EXPLAIN

'.

Program terminated with signal 6, Aborted.
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install

glibc-2.17-222.el7.x86_64 keyutils-libs-1.5.8-3.el7.x86_64
krb5-libs-1.15.1-19.el7.x86_64 libcom_err-1.42.9-12.el7_5.x86_64
libgcc-4.8.5-39.el7.x86_64 libselinux-2.5-12.el7.x86_64
openssl-libs-1.0.2k-19.el7.x86_64 pcre-8.32-17.el7.x86_64
zlib-1.2.7-17.el7.x86_64

(gdb) bt
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
#1 0x00007f7d339bb968 in abort () from /lib64/libc.so.6
#2 0x0000000000b0fbc3 in ExceptionalCondition

(conditionName=0xcbda10 "part_index >= 0", errorType=0xcbd1c3
"FailedAssertion", fileName=0xcbd2fe "partbounds.c", lineNumber=1957)

at assert.c:69
#3 0x0000000000892aa1 in is_dummy_partition (rel=0x19b37c0,

part_index=-1) at partbounds.c:1957

#4 0x00000000008919bd in merge_list_bounds (partnatts=1,

partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0,
inner_rel=0x1922938, jointype=JOIN_INNER,

outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at

partbounds.c:1529

#5 0x00000000008910de in partition_bounds_merge (partnatts=1,

partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0,
inner_rel=0x1922938, jointype=JOIN_INNER,

outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at

partbounds.c:1223

#6 0x000000000082c41a in compute_partition_bounds (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
parent_sjinfo=0x7fffd67752a0, parts1=0x7fffd67751b0,

parts2=0x7fffd67751a8) at joinrels.c:1644
#7 0x000000000082bc34 in try_partitionwise_join (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
parent_sjinfo=0x7fffd67752a0, parent_restrictlist=0x1ab3318)

at joinrels.c:1402
#8 0x000000000082aea2 in populate_joinrel_with_paths

(root=0x1a19ed0, rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
sjinfo=0x7fffd67752a0, restrictlist=0x1ab3318)

at joinrels.c:926
#9 0x000000000082a8f5 in make_join_rel (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938) at joinrels.c:760

#10 0x0000000000829e03 in make_rels_by_clause_joins (root=0x1a19ed0,

old_rel=0x19b37c0, other_rels_list=0x1ab2970, other_rels=0x1ab2990) at
joinrels.c:312

#11 0x00000000008298d9 in join_search_one_level (root=0x1a19ed0,

level=2) at joinrels.c:123

#12 0x000000000080c566 in standard_join_search (root=0x1a19ed0,

levels_needed=2, initial_rels=0x1ab2970) at allpaths.c:3020

#13 0x000000000080c4df in make_rel_from_joinlist (root=0x1a19ed0,

joinlist=0x199d538) at allpaths.c:2951

#14 0x000000000080816b in make_one_rel (root=0x1a19ed0,

joinlist=0x199d538) at allpaths.c:228

#15 0x000000000084491d in query_planner (root=0x1a19ed0,

qp_callback=0x84a538 <standard_qp_callback>, qp_extra=0x7fffd6775630) at
planmain.c:276

#16 0x0000000000847040 in grouping_planner (root=0x1a19ed0,

tuple_fraction=0) at planner.c:1447

#17 0x0000000000846709 in subquery_planner (glob=0x19b39d8,

parse=0x1aaa290, parent_root=0x0, hasRecursion=false, tuple_fraction=0) at
planner.c:1025

#18 0x0000000000844f3e in standard_planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at planner.c:406

#19 0x0000000000844ce9 in planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at planner.c:277

#20 0x0000000000978483 in pg_plan_query (querytree=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at postgres.c:847

#21 0x00000000006937fc in ExplainOneQuery (query=0x1aaa290,

cursorOptions=2048, into=0x0, es=0x19b36f0,

queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

params=0x0, queryEnv=0x0) at explain.c:397
#22 0x0000000000693351 in ExplainQuery (pstate=0x197c410,

stmt=0x1aaa0b0, params=0x0, dest=0x197c378) at explain.c:281

#23 0x00000000009811fa in standard_ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,

queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at utility.c:845

#24 0x00000000009809ec in ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,

queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at utility.c:527

#25 0x000000000097f636 in PortalRunUtility (portal=0x1893b40,

pstmt=0x1a0bfc8, isTopLevel=true, setHoldSnapshot=true, dest=0x197c378,
qc=0x7fffd6775f90) at pquery.c:1147

#26 0x000000000097f3a5 in FillPortalStore (portal=0x1893b40,

isTopLevel=true) at pquery.c:1026

#27 0x000000000097ed11 in PortalRun (portal=0x1893b40,

count=9223372036854775807, isTopLevel=true, run_once=true, dest=0x1a0c0b8,
altdest=0x1a0c0b8, qc=0x7fffd6776150) at pquery.c:758

#28 0x0000000000978aa5 in exec_simple_query (

Thanks & Regards,
Rajkumar Raghuwanshi

On Fri, Sep 3, 2021 at 7:17 PM Amit Langote <amitlangote09@gmail.com>

wrote:

On Wed, Sep 1, 2021 at 2:31 PM Amit Langote <amitlangote09@gmail.com>

wrote:

On Tue, Aug 31, 2021 at 8:02 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

The attached patch also fixes the above comments.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b,

c);

create table p01 partition of p0 for values in ((1, 1, true),

(NULL, 1, false));

create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

I guess that may be due to the following newly added code being

incomplete:

+/*
+ * get_partition_bound_null_index
+ *
+ * Returns the partition index of the partition bound which

accepts NULL.

+ */
+int
+get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+{
+   int i = 0;
+   int j = 0;
+
+   if (!boundinfo->isnulls)
+       return -1;
-           if (!val->constisnull)
-               count++;
+   for (i = 0; i < boundinfo->ndatums; i++)
+   {
+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)
+       {
+           if (boundinfo->isnulls[i][j])
+               return boundinfo->indexes[i];
}
}

+ return -1;
+}

Maybe this function needs to return a "bitmapset" of indexes,

because

multiple partitions can now contain NULL values.

Some other issues I noticed and suggestions for improvement:

+/*
+ * checkForDuplicates
+ *
+ * Returns TRUE if the list bound element is already present in

the list of

+ * list bounds, FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)

This function name may be too generic. Given that it is specific

to

implementing list bound de-duplication, maybe the following

signature

is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Also, better if the function comment mentions those parameter

names, like:

"Returns TRUE if the list bound element 'new_bound' is already

present

in the target list 'list_bounds', FALSE otherwise."

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw

grammar

+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

+ colname[i] = (char *) palloc0(NAMEDATALEN *

sizeof(char));

+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd

statement

overwrites its pointer by the pointer to the string palloc'd by
get_attname().

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate,

exprLocation((Node

*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how

about

writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Will send more comments in a bit...

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
*/

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+ all_values[j]->values = (Datum *) palloc0(key->partnatts

*

sizeof(Datum));
+ all_values[j]->isnulls = (bool *) palloc0(key->partnatts

*

sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts *

sizeof(Datum));

Missing a newline between these two statements.

BTW, I noticed that the boundDatums variable is no longer used in
create_list_bounds. I traced back its origin and found that a recent
commit 53d86957e98 introduced it to implement an idea to reduce the
finer-grained pallocs that were being done in create_list_bounds().

I

don't think that this patch needs to throw away that work. You can
make it work as the attached delta patch that applies on top of v3.
Please check.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in

partition_bounds_equal()

needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously

mentioned.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the above
code stanza which appears to implement a short-cut for this long-form
logic.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

(Skipped looking at merge_list_bounds() and related changes for now

as

I see a lot of TODOs remain to be done.)

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the

logic

for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the same
goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits

the

IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a few
other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

I still have more than half of v3 left to look at, so will continue
looking. In the meantime, please check the changes I suggested,
including the delta patch, and let me know your thoughts.

--
Amit Langote
EDB: http://www.enterprisedb.com

#28Amit Langote
amitlangote09@gmail.com
In reply to: Rajkumar Raghuwanshi (#26)
Re: Multi-Column List Partitioning

Hi Rajkumar,

On Mon, Oct 11, 2021 at 2:36 PM Rajkumar Raghuwanshi
<rajkumar.raghuwanshi@enterprisedb.com> wrote:

Thanks for the patch, it applied cleanly and fixed the reported issue. I observed another case where
In case of multi-col list partition on the same column query is not picking partition wise join. Is this expected?

CREATE TABLE plt1 (a int, b int, c varchar) PARTITION BY LIST(c,c);
CREATE TABLE plt1_p1 PARTITION OF plt1 FOR VALUES IN (('0001','0001'),('0002','0002'),('0003','0003'));
CREATE TABLE plt1_p2 PARTITION OF plt1 FOR VALUES IN (('0004','0004'),('0005','0005'),('0006','0006'));
CREATE TABLE plt1_p3 PARTITION OF plt1 DEFAULT;
INSERT INTO plt1 SELECT i, i % 47, to_char(i % 11, 'FM0000') FROM generate_series(0, 500) i WHERE i % 11 NOT IN (0,10);
ANALYSE plt1;
CREATE TABLE plt2 (a int, b int, c varchar) PARTITION BY LIST(c,c);
CREATE TABLE plt2_p1 PARTITION OF plt2 FOR VALUES IN (('0001','0001'),('0002','0002'),('0003','0003'));
CREATE TABLE plt2_p2 PARTITION OF plt2 FOR VALUES IN (('0004','0004'),('0005','0005'),('0006','0006'));
CREATE TABLE plt2_p3 PARTITION OF plt2 DEFAULT;
INSERT INTO plt2 SELECT i, i % 47, to_char(i % 11, 'FM0000') FROM generate_series(0, 500) i WHERE i % 11 NOT IN (0,10);
ANALYSE plt2;
SET enable_partitionwise_join TO true;
EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.a, t2.c FROM plt1 t1 INNER JOIN plt2 t2 ON t1.c = t2.c;

postgres=# EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.a, t2.c FROM plt1 t1 INNER JOIN plt2 t2 ON t1.c = t2.c;
QUERY PLAN
--------------------------------------------
Hash Join
Hash Cond: ((t1.c)::text = (t2.c)::text)
-> Append
-> Seq Scan on plt1_p1 t1_1
-> Seq Scan on plt1_p2 t1_2
-> Seq Scan on plt1_p3 t1_3
-> Hash
-> Append
-> Seq Scan on plt2_p1 t2_1
-> Seq Scan on plt2_p2 t2_2
-> Seq Scan on plt2_p3 t2_3
(11 rows)

Interesting test case.

I think this might be an *existing* limitation of the code that
compares join clauses against the partition key(s) to determine if
partition-wise join should be considered. The clause t1.c = t2.c
should have been matched with both of the partition keys (c, c), but
it is not given the way have_partkey_equi_join() is currently coded.
I suspect you'd get the same behavior if you'd used a RANGE
partitioned table with keys (c, c). Not sure though if it'd be
worthwhile to fix that coding to cater to this odd partition key
setting.

--
Amit Langote
EDB: http://www.enterprisedb.com

#29Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Rajkumar Raghuwanshi (#27)
1 attachment(s)
Re: Multi-Column List Partitioning

While testing further I got a crash with partition wise join enabled for

multi-col list partitions. please find test case & stack-trace below.

Thanks for sharing. I have fixed the issue in the attached patch.

Thanks & Regards,
Nitin Jadhav

On Mon, Oct 11, 2021 at 4:12 PM Rajkumar Raghuwanshi <
rajkumar.raghuwanshi@enterprisedb.com> wrote:

Show quoted text

Hi Nitin,

While testing further I got a crash with partition wise join enabled for
multi-col list partitions. please find test case & stack-trace below.

SET enable_partitionwise_join TO on;
CREATE TABLE plt1 (c varchar, d varchar) PARTITION BY LIST(c,d);
CREATE TABLE plt1_p1 PARTITION OF plt1 FOR VALUES IN
(('0001','0001'),('0002','0002'),(NULL,NULL));
CREATE TABLE plt1_p2 PARTITION OF plt1 FOR VALUES IN
(('0004','0004'),('0005','0005'),('0006','0006'));
INSERT INTO plt1 SELECT to_char(i % 11, 'FM0000'), to_char(i % 11,
'FM0000') FROM generate_series(0, 500) i WHERE i % 11 NOT IN (0,10,3,7,8,9);
INSERT INTO plt1 SELECT NULL,NULL FROM generate_series(0, 500) i WHERE i %
11 IN (3);
ANALYSE plt1;
CREATE TABLE plt2 (c varchar, d varchar) PARTITION BY LIST(c,d);
CREATE TABLE plt2_p1 PARTITION OF plt2 FOR VALUES IN
(('0001','0001'),('0002','0002'));
CREATE TABLE plt2_p2 PARTITION OF plt2 FOR VALUES IN
(('0004','0004'),('0005','0005'),('0006','0006'));
CREATE TABLE plt2_p3 PARTITION OF plt2 DEFAULT;
INSERT INTO plt2 SELECT to_char(i % 11, 'FM0000'), to_char(i % 11,
'FM0000') FROM generate_series(0, 500) i WHERE i % 11 NOT IN (0,10,3);
INSERT INTO plt2 SELECT NULL,NULL FROM generate_series(0, 500) i WHERE i %
11 IN (3);
ANALYSE plt2;

EXPLAIN (COSTS OFF)
SELECT t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON
(t1.c = t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d =
t3.d);

postgres=# EXPLAIN (COSTS OFF)
postgres-# SELECT t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN
plt2 t2 ON (t1.c = t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c
AND t2.d = t3.d);
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
The connection to the server was lost. Attempting reset: Failed.
!?> \q
[edb@localhost bin]$ gdb -q -c data/core.66926 postgres
Reading symbols from
/home/edb/WORK/pg_src/PG_TEMP/postgresql/inst/bin/postgres...done.
[New LWP 66926]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Core was generated by `postgres: edb postgres [local] EXPLAIN
'.
Program terminated with signal 11, Segmentation fault.
#0 0x000000000082be39 in is_dummy_rel (rel=0x40) at joinrels.c:1221
1221 if (rel->pathlist == NIL)
(gdb) bt
#0 0x000000000082be39 in is_dummy_rel (rel=0x40) at joinrels.c:1221
#1 0x000000000089341c in is_dummy_partition (rel=0x2f86e88, part_index=2)
at partbounds.c:1959
#2 0x0000000000891d38 in merge_list_bounds (partnatts=2,
partsupfunc=0x2f70058, partcollation=0x2fd3c98, outer_rel=0x2f86e88,
inner_rel=0x2fd4368, jointype=JOIN_LEFT,
outer_parts=0x7ffea91f8cc0, inner_parts=0x7ffea91f8cb8) at
partbounds.c:1325
#3 0x0000000000891991 in partition_bounds_merge (partnatts=2,
partsupfunc=0x2f70058, partcollation=0x2fd3c98, outer_rel=0x2f86e88,
inner_rel=0x2fd4368, jointype=JOIN_LEFT,
outer_parts=0x7ffea91f8cc0, inner_parts=0x7ffea91f8cb8) at
partbounds.c:1198
#4 0x000000000082cc5a in compute_partition_bounds (root=0x2f9e910,
rel1=0x2f86e88, rel2=0x2fd4368, joinrel=0x2fae388, parent_sjinfo=0x2f7dfa8,
parts1=0x7ffea91f8cc0,
parts2=0x7ffea91f8cb8) at joinrels.c:1644
#5 0x000000000082c474 in try_partitionwise_join (root=0x2f9e910,
rel1=0x2f86e88, rel2=0x2fd4368, joinrel=0x2fae388, parent_sjinfo=0x2f7dfa8,
parent_restrictlist=0x2fae650)
at joinrels.c:1402
#6 0x000000000082b6e2 in populate_joinrel_with_paths (root=0x2f9e910,
rel1=0x2f86e88, rel2=0x2fd4368, joinrel=0x2fae388, sjinfo=0x2f7dfa8,
restrictlist=0x2fae650) at joinrels.c:926
#7 0x000000000082b135 in make_join_rel (root=0x2f9e910, rel1=0x2f86e88,
rel2=0x2fd4368) at joinrels.c:760
#8 0x000000000082a643 in make_rels_by_clause_joins (root=0x2f9e910,
old_rel=0x2f86e88, other_rels_list=0x2f90148, other_rels=0x2f90160) at
joinrels.c:312
#9 0x000000000082a119 in join_search_one_level (root=0x2f9e910, level=3)
at joinrels.c:123
#10 0x000000000080cd97 in standard_join_search (root=0x2f9e910,
levels_needed=3, initial_rels=0x2f90148) at allpaths.c:3020
#11 0x000000000080cd10 in make_rel_from_joinlist (root=0x2f9e910,
joinlist=0x2fd7550) at allpaths.c:2951
#12 0x000000000080899a in make_one_rel (root=0x2f9e910,
joinlist=0x2fd7550) at allpaths.c:228
#13 0x000000000084516a in query_planner (root=0x2f9e910,
qp_callback=0x84ad85 <standard_qp_callback>, qp_extra=0x7ffea91f9140) at
planmain.c:276
#14 0x000000000084788d in grouping_planner (root=0x2f9e910,
tuple_fraction=0) at planner.c:1447
#15 0x0000000000846f56 in subquery_planner (glob=0x2fa0c08,
parse=0x2f56d30, parent_root=0x0, hasRecursion=false, tuple_fraction=0) at
planner.c:1025
#16 0x000000000084578b in standard_planner (parse=0x2f56d30,
query_string=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);",
cursorOptions=2048, boundParams=0x0) at planner.c:406
#17 0x0000000000845536 in planner (parse=0x2f56d30,
query_string=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);",
cursorOptions=2048, boundParams=0x0) at planner.c:277
#18 0x0000000000978faf in pg_plan_query (querytree=0x2f56d30,
query_string=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);",
cursorOptions=2048, boundParams=0x0) at postgres.c:847
#19 0x0000000000693e50 in ExplainOneQuery (query=0x2f56d30,
cursorOptions=2048, into=0x0, es=0x2fa0920,
queryString=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);",
params=0x0, queryEnv=0x0) at explain.c:397
#20 0x00000000006939a5 in ExplainQuery (pstate=0x2f9e0a0, stmt=0x2f56b50,
params=0x0, dest=0x2f9e008) at explain.c:281
#21 0x0000000000981de8 in standard_ProcessUtility (pstmt=0x2fd2220,
queryString=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);",
readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,
queryEnv=0x0, dest=0x2f9e008, qc=0x7ffea91f9aa0) at utility.c:862
#22 0x0000000000981585 in ProcessUtility (pstmt=0x2fd2220,
queryString=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);",
readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL, params=0x0,
queryEnv=0x0, dest=0x2f9e008, qc=0x7ffea91f9aa0) at utility.c:527
#23 0x00000000009801ba in PortalRunUtility (portal=0x2f10180,
pstmt=0x2fd2220, isTopLevel=true, setHoldSnapshot=true, dest=0x2f9e008,
qc=0x7ffea91f9aa0) at pquery.c:1155
#24 0x000000000097ff20 in FillPortalStore (portal=0x2f10180,
isTopLevel=true) at pquery.c:1028
#25 0x000000000097f883 in PortalRun (portal=0x2f10180,
count=9223372036854775807, isTopLevel=true, run_once=true, dest=0x2fd2310,
altdest=0x2fd2310, qc=0x7ffea91f9c60) at pquery.c:760
#26 0x00000000009795d1 in exec_simple_query (
query_string=0x2eadcd0 "EXPLAIN (COSTS OFF)\nSELECT
t1.c,t2.c,t3.c,t1.d,t2.d,t3.d FROM plt1 t1 INNER JOIN plt2 t2 ON (t1.c =
t2.c AND t1.d = t2.d) LEFT JOIN plt1 t3 on (t2.c = t3.c AND t2.d = t3.d);")
at postgres.c:1214
#27 0x000000000097da8d in PostgresMain (dbname=0x2ed8068 "postgres",
username=0x2ed8048 "edb") at postgres.c:4497
#28 0x00000000008b9699 in BackendRun (port=0x2ecfd00) at postmaster.c:4560

Thanks & Regards,
Rajkumar Raghuwanshi

On Mon, Oct 11, 2021 at 11:05 AM Rajkumar Raghuwanshi <
rajkumar.raghuwanshi@enterprisedb.com> wrote:

Thanks for the patch, it applied cleanly and fixed the reported issue. I
observed another case where
In case of multi-col list partition on the same column query is not
picking partition wise join. Is this expected?

CREATE TABLE plt1 (a int, b int, c varchar) PARTITION BY LIST(c,c);
CREATE TABLE plt1_p1 PARTITION OF plt1 FOR VALUES IN
(('0001','0001'),('0002','0002'),('0003','0003'));
CREATE TABLE plt1_p2 PARTITION OF plt1 FOR VALUES IN
(('0004','0004'),('0005','0005'),('0006','0006'));
CREATE TABLE plt1_p3 PARTITION OF plt1 DEFAULT;
INSERT INTO plt1 SELECT i, i % 47, to_char(i % 11, 'FM0000') FROM
generate_series(0, 500) i WHERE i % 11 NOT IN (0,10);
ANALYSE plt1;
CREATE TABLE plt2 (a int, b int, c varchar) PARTITION BY LIST(c,c);
CREATE TABLE plt2_p1 PARTITION OF plt2 FOR VALUES IN
(('0001','0001'),('0002','0002'),('0003','0003'));
CREATE TABLE plt2_p2 PARTITION OF plt2 FOR VALUES IN
(('0004','0004'),('0005','0005'),('0006','0006'));
CREATE TABLE plt2_p3 PARTITION OF plt2 DEFAULT;
INSERT INTO plt2 SELECT i, i % 47, to_char(i % 11, 'FM0000') FROM
generate_series(0, 500) i WHERE i % 11 NOT IN (0,10);
ANALYSE plt2;
SET enable_partitionwise_join TO true;
EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.a, t2.c FROM plt1 t1 INNER JOIN
plt2 t2 ON t1.c = t2.c;

postgres=# EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.a, t2.c FROM plt1 t1
INNER JOIN plt2 t2 ON t1.c = t2.c;
QUERY PLAN
--------------------------------------------
Hash Join
Hash Cond: ((t1.c)::text = (t2.c)::text)
-> Append
-> Seq Scan on plt1_p1 t1_1
-> Seq Scan on plt1_p2 t1_2
-> Seq Scan on plt1_p3 t1_3
-> Hash
-> Append
-> Seq Scan on plt2_p1 t2_1
-> Seq Scan on plt2_p2 t2_2
-> Seq Scan on plt2_p3 t2_3
(11 rows)

Thanks & Regards,
Rajkumar Raghuwanshi

On Thu, Oct 7, 2021 at 6:03 PM Nitin Jadhav <
nitinjadhavpostgres@gmail.com> wrote:

Thanks Rajkumar for testing.

I think it should throw an error as the partition by list has only 1

column but we are giving 2 values.

I also agree that it should throw an error in the above case. Fixed the
issue in the attached patch. Also added related test cases to the
regression test suite.

also if you see \d+ showing plt1_p1 partition value as ‘(0001,0001)’

instead of ('0001','0001').

Now throwing errors in the initial stage, this case doesn't arise.

Please share if you find any other issues.

Thanks & Regards,
Nitin Jadhav

On Thu, Oct 7, 2021 at 4:05 PM Rajkumar Raghuwanshi <
rajkumar.raghuwanshi@enterprisedb.com> wrote:

Thanks Nitin,

v4 patches applied cleanly and make check is passing now. While testing
further I observed that if multiple values are given for a single
column list partition it is not giving error instead it is changing
values itself. Please find the example below.

postgres=# CREATE TABLE plt1 (a int, b varchar) PARTITION BY LIST(b);
CREATE TABLE
postgres=# CREATE TABLE plt1_p1 PARTITION OF plt1 FOR VALUES IN
(('0001','0001'),('0002','0002'));
CREATE TABLE
postgres=# \d+ plt1;
Partitioned table
"public.plt1"
Column | Type | Collation | Nullable | Default | Storage
| Compression | Stats target | Description

--------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
a | integer | | | | plain
| | |
b | character varying | | | | extended
| | |
Partition key: LIST (b)
Partitions: plt1_p1 FOR VALUES IN ('(0001,0001)', '(0002,0002)')

I think it should throw an error as the partition by list has only 1
column but we are giving 2 values.
also if you see \d+ showing plt1_p1 partition value as ‘(0001,0001)’
instead of ('0001','0001').

Thanks & Regards,
Rajkumar Raghuwanshi

On Sun, Oct 3, 2021 at 1:52 AM Nitin Jadhav <
nitinjadhavpostgres@gmail.com> wrote:

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is

failing with below errors.

Thanks Rajkumar for testing.

Here's a v2 of the delta patch that should fix both of these test
failures. As I mentioned in my last reply, my delta patch fixed what
I think were problems in Nitin's v3 patch but were not complete by
themselves. Especially, I hadn't bothered to investigate various /*
TODO: handle multi-column list partitioning */ sites to deal with my
own changes.

Thanks Rajkumar for testing and Thank you Amit for working on v2 of
the delta patch. Actually I had done the code changes related to
partition-wise join and I was in the middle of fixing the review
comments, So I could not share the patch. Anyways thanks for your
efforts.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b, c);
create table p01 partition of p0 for values in ((1, 1, true), (NULL,

1, false));

create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

In the attached updated version, I've dealt with some of those such
that at least the existing cases exercising partition pruning and
partition wise joins now pass.

wrt partition pruning, I have checked the output of the above case
with the v2 version of the delta patch and without that. The output
remains same. Kindly let me know if I am missing something. But I feel
the above output is correct as the partition p01 is the only partition
which contains NULL value for column a, hence it is showing "Seq scan
on p01" in the output. Kindly correct me if I am wrong. I feel the
code changes related to 'null_keys' is not required, hence not
incorporated that in the attached patch.

wrt partition-wise join, I had run the regression test (with new cases
related to partition-wise join) on v2 of the delta patch and observed
the crash. Hence I have not incorporated the partition-wise join
related code from v2 of delta patch to main v4 patch. Instead I have
added the partition-wise join related code done by me in the attached
patch. Please share your thoughts and if possible we can improvise the
code. Rest of the changes looks good to me and I have incorporated
that in the attached patch.

I guess that may be due to the following newly added code being

incomplete:

Maybe this function needs to return a "bitmapset" of indexes, because
multiple partitions can now contain NULL values.

I feel this function is not required at all as we are not separating
the non null and null partitions now. Removed in the attached patch.
Also removed the "scan_null' variable from the structure
"PruneStepResult" and cleaned up the corresponding code blocks.

This function name may be too generic. Given that it is specific to
implementing list bound de-duplication, maybe the following signature
is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Yes. The function name looks more generic. How about using
"isListBoundDuplicated()"? I have used this name in the patch. Please
let me know if that does not look correct.

Also, better if the function comment mentions those parameter names,

like:

"Returns TRUE if the list bound element 'new_bound' is already

present

in the target list 'list_bounds', FALSE otherwise."

Fixed.

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw

grammar

+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

Fixed.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

Fixed in all the places.

+ colname[i] = (char *) palloc0(NAMEDATALEN *

sizeof(char));

+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd

statement

overwrites its pointer by the pointer to the string palloc'd by
get_attname().

Removed the 1st statement as it is not required.

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

Fixed in all the places.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("Invalid list bound specification"),
+                       parser_errposition(pstate, exprLocation((Node
*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

Fixed.

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how about
writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

This looks good. I have changed in the patch.

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Yes. PartitionListBound looks more appropriate and it also matches the
similar structures of the other partition strategies.

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Retained the old comment.

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
*/

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

Fixed.

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+ all_values[j]->values = (Datum *) palloc0(key->partnatts

*

sizeof(Datum));
+ all_values[j]->isnulls = (bool *) palloc0(key->partnatts

*

sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts *

sizeof(Datum));

Missing a newline between these two statements.

Fixed. Made necessary changes to keep the intent of existing code.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in

partition_bounds_equal()

needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously

mentioned.

As mentioned earlier, removed the functionality of
get_partition_bound_null_index(), hence the above condition is not
required and removed.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the above
code stanza which appears to implement a short-cut for this long-form
logic.

Yes. May be we could have ignored the above code stanza if we would
have comparing the null indexes using get_partition_bound_null_index()
in the beginning of the function. But hence we are not separating the
non null partitions and null partitions, I would like to keep the
logic in the inner loop as we are doing it for non null bound values
in the above code stanza, just to give a feel that null bound values
are also handled the same way as non null values. Please correct me if
I am wrong.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

Fixed.

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

Thanks for the suggestion. I have changed as above.

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the

logic

for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the same
goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

Actually I had written a separate function because of the complexity.
Now I have understood that since the objective is same, it should be
done in a single function irrespective of complexity.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits

the

IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a few
other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

Thank you very much for your efforts. The changes looks good to me and
I have incorporated these changes in the attached patch.

I have completed the coding for all the TODOs and hence removed in the
patch. The naming conventions used for function/variable names varies
across the files. Some places it is like 'namesLikeThis' and in some
place it is like 'names_like_this'. I have used the naming conventions
based on the surrounding styles used. I am happy to change those if
required.

I have verified 'make check' with the attached patch and it is working
fine.

Thanks & Regards,
Nitin Jadhav

On Mon, Sep 13, 2021 at 3:47 PM Rajkumar Raghuwanshi
<rajkumar.raghuwanshi@enterprisedb.com> wrote:

On PG head + Nitin's v3 patch + Amit's Delta patch. Make check is

failing with below errors.

--inherit.sql is failing with error :"ERROR: negative bitmapset

member not allowed"

update mlparted_tab mlp set c = 'xxx'
from
(select a from some_tab union all select a+1 from some_tab) ss (a)
where (mlp.a = ss.a and mlp.b = 'b') or mlp.a = 3;
ERROR: negative bitmapset member not allowed

--partition_join.sql is crashing with enable_partitionwise_join set

to true.

CREATE TABLE plt1_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt1_adv_p1 PARTITION OF plt1_adv FOR VALUES IN

('0001', '0003');

CREATE TABLE plt1_adv_p2 PARTITION OF plt1_adv FOR VALUES IN

('0004', '0006');

CREATE TABLE plt1_adv_p3 PARTITION OF plt1_adv FOR VALUES IN

('0008', '0009');

INSERT INTO plt1_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM

generate_series(1, 299) i WHERE i % 10 IN (1, 3, 4, 6, 8, 9);

ANALYZE plt1_adv;
CREATE TABLE plt2_adv (a int, b int, c text) PARTITION BY LIST (c);
CREATE TABLE plt2_adv_p1 PARTITION OF plt2_adv FOR VALUES IN

('0002', '0003');

CREATE TABLE plt2_adv_p2 PARTITION OF plt2_adv FOR VALUES IN

('0004', '0006');

CREATE TABLE plt2_adv_p3 PARTITION OF plt2_adv FOR VALUES IN

('0007', '0009');

INSERT INTO plt2_adv SELECT i, i, to_char(i % 10, 'FM0000') FROM

generate_series(1, 299) i WHERE i % 10 IN (2, 3, 4, 6, 7, 9);

ANALYZE plt2_adv;
-- inner join
EXPLAIN (COSTS OFF)
SELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv

t2 ON (t1.a = t2.a AND t1.c = t2.c) WHERE t1.b < 10 ORDER BY t1.a;

server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost

--stack-trace
Core was generated by `postgres: edb regression [local] EXPLAIN

'.

Program terminated with signal 6, Aborted.
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install

glibc-2.17-222.el7.x86_64 keyutils-libs-1.5.8-3.el7.x86_64
krb5-libs-1.15.1-19.el7.x86_64 libcom_err-1.42.9-12.el7_5.x86_64
libgcc-4.8.5-39.el7.x86_64 libselinux-2.5-12.el7.x86_64
openssl-libs-1.0.2k-19.el7.x86_64 pcre-8.32-17.el7.x86_64
zlib-1.2.7-17.el7.x86_64

(gdb) bt
#0 0x00007f7d339ba277 in raise () from /lib64/libc.so.6
#1 0x00007f7d339bb968 in abort () from /lib64/libc.so.6
#2 0x0000000000b0fbc3 in ExceptionalCondition

(conditionName=0xcbda10 "part_index >= 0", errorType=0xcbd1c3
"FailedAssertion", fileName=0xcbd2fe "partbounds.c", lineNumber=1957)

at assert.c:69
#3 0x0000000000892aa1 in is_dummy_partition (rel=0x19b37c0,

part_index=-1) at partbounds.c:1957

#4 0x00000000008919bd in merge_list_bounds (partnatts=1,

partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0,
inner_rel=0x1922938, jointype=JOIN_INNER,

outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at

partbounds.c:1529

#5 0x00000000008910de in partition_bounds_merge (partnatts=1,

partsupfunc=0x1922798, partcollation=0x1922738, outer_rel=0x19b37c0,
inner_rel=0x1922938, jointype=JOIN_INNER,

outer_parts=0x7fffd67751b0, inner_parts=0x7fffd67751a8) at

partbounds.c:1223

#6 0x000000000082c41a in compute_partition_bounds (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
parent_sjinfo=0x7fffd67752a0, parts1=0x7fffd67751b0,

parts2=0x7fffd67751a8) at joinrels.c:1644
#7 0x000000000082bc34 in try_partitionwise_join (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
parent_sjinfo=0x7fffd67752a0, parent_restrictlist=0x1ab3318)

at joinrels.c:1402
#8 0x000000000082aea2 in populate_joinrel_with_paths

(root=0x1a19ed0, rel1=0x19b37c0, rel2=0x1922938, joinrel=0x1ab7f30,
sjinfo=0x7fffd67752a0, restrictlist=0x1ab3318)

at joinrels.c:926
#9 0x000000000082a8f5 in make_join_rel (root=0x1a19ed0,

rel1=0x19b37c0, rel2=0x1922938) at joinrels.c:760

#10 0x0000000000829e03 in make_rels_by_clause_joins (root=0x1a19ed0,

old_rel=0x19b37c0, other_rels_list=0x1ab2970, other_rels=0x1ab2990) at
joinrels.c:312

#11 0x00000000008298d9 in join_search_one_level (root=0x1a19ed0,

level=2) at joinrels.c:123

#12 0x000000000080c566 in standard_join_search (root=0x1a19ed0,

levels_needed=2, initial_rels=0x1ab2970) at allpaths.c:3020

#13 0x000000000080c4df in make_rel_from_joinlist (root=0x1a19ed0,

joinlist=0x199d538) at allpaths.c:2951

#14 0x000000000080816b in make_one_rel (root=0x1a19ed0,

joinlist=0x199d538) at allpaths.c:228

#15 0x000000000084491d in query_planner (root=0x1a19ed0,

qp_callback=0x84a538 <standard_qp_callback>, qp_extra=0x7fffd6775630) at
planmain.c:276

#16 0x0000000000847040 in grouping_planner (root=0x1a19ed0,

tuple_fraction=0) at planner.c:1447

#17 0x0000000000846709 in subquery_planner (glob=0x19b39d8,

parse=0x1aaa290, parent_root=0x0, hasRecursion=false, tuple_fraction=0) at
planner.c:1025

#18 0x0000000000844f3e in standard_planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at planner.c:406

#19 0x0000000000844ce9 in planner (parse=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at planner.c:277

#20 0x0000000000978483 in pg_plan_query (querytree=0x1aaa290,
query_string=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;", cursorOptions=2048,
boundParams=0x0) at postgres.c:847

#21 0x00000000006937fc in ExplainOneQuery (query=0x1aaa290,

cursorOptions=2048, into=0x0, es=0x19b36f0,

queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

params=0x0, queryEnv=0x0) at explain.c:397
#22 0x0000000000693351 in ExplainQuery (pstate=0x197c410,

stmt=0x1aaa0b0, params=0x0, dest=0x197c378) at explain.c:281

#23 0x00000000009811fa in standard_ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL,

params=0x0, queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at
utility.c:845

#24 0x00000000009809ec in ProcessUtility (pstmt=0x1a0bfc8,
queryString=0x1830fa0 "EXPLAIN (COSTS OFF)\nSELECT t1.a, t1.c,

t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c
= t2.c) WHERE t1.b < 10 ORDER BY t1.a;",

readOnlyTree=false, context=PROCESS_UTILITY_TOPLEVEL,

params=0x0, queryEnv=0x0, dest=0x197c378, qc=0x7fffd6775f90) at
utility.c:527

#25 0x000000000097f636 in PortalRunUtility (portal=0x1893b40,

pstmt=0x1a0bfc8, isTopLevel=true, setHoldSnapshot=true, dest=0x197c378,
qc=0x7fffd6775f90) at pquery.c:1147

#26 0x000000000097f3a5 in FillPortalStore (portal=0x1893b40,

isTopLevel=true) at pquery.c:1026

#27 0x000000000097ed11 in PortalRun (portal=0x1893b40,

count=9223372036854775807, isTopLevel=true, run_once=true, dest=0x1a0c0b8,
altdest=0x1a0c0b8, qc=0x7fffd6776150) at pquery.c:758

#28 0x0000000000978aa5 in exec_simple_query (

Thanks & Regards,
Rajkumar Raghuwanshi

On Fri, Sep 3, 2021 at 7:17 PM Amit Langote <amitlangote09@gmail.com>

wrote:

On Wed, Sep 1, 2021 at 2:31 PM Amit Langote <

amitlangote09@gmail.com> wrote:

On Tue, Aug 31, 2021 at 8:02 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

The attached patch also fixes the above comments.

I noticed that multi-column list partitions containing NULLs don't
work correctly with partition pruning yet.

create table p0 (a int, b text, c bool) partition by list (a, b,

c);

create table p01 partition of p0 for values in ((1, 1, true),

(NULL, 1, false));

create table p02 partition of p0 for values in ((1, NULL, false));
explain select * from p0 where a is null;
QUERY PLAN
--------------------------------------------------------
Seq Scan on p01 p0 (cost=0.00..22.50 rows=6 width=37)
Filter: (a IS NULL)
(2 rows)

I guess that may be due to the following newly added code being

incomplete:

+/*
+ * get_partition_bound_null_index
+ *
+ * Returns the partition index of the partition bound which

accepts NULL.

+ */
+int
+get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+{
+   int i = 0;
+   int j = 0;
+
+   if (!boundinfo->isnulls)
+       return -1;
-           if (!val->constisnull)
-               count++;
+   for (i = 0; i < boundinfo->ndatums; i++)
+   {
+       //TODO: Handle for multi-column cases
+       for (j = 0; j < 1; j++)
+       {
+           if (boundinfo->isnulls[i][j])
+               return boundinfo->indexes[i];
}
}

+ return -1;
+}

Maybe this function needs to return a "bitmapset" of indexes,

because

multiple partitions can now contain NULL values.

Some other issues I noticed and suggestions for improvement:

+/*
+ * checkForDuplicates
+ *
+ * Returns TRUE if the list bound element is already present in

the list of

+ * list bounds, FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)

This function name may be too generic. Given that it is specific

to

implementing list bound de-duplication, maybe the following

signature

is more appropriate:

static bool
checkListBoundDuplicated(List *list_bounds, List *new_bound)

Also, better if the function comment mentions those parameter

names, like:

"Returns TRUE if the list bound element 'new_bound' is already

present

in the target list 'list_bounds', FALSE otherwise."

+/*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the

raw grammar

+ * representation.

A sentence about the result format would be helpful, like:

The result is a List of Lists of Const nodes to account for the
partition key possibly containing more than one column.

+ int i = 0;
+ int j = 0;

Better to initialize such loop counters closer to the loop.

+ colname[i] = (char *) palloc0(NAMEDATALEN *

sizeof(char));

+           colname[i] = get_attname(RelationGetRelid(parent),
+                                    key->partattrs[i], false);

The palloc in the 1st statement is wasteful, because the 2nd

statement

overwrites its pointer by the pointer to the string palloc'd by
get_attname().

+ ListCell *cell2 = NULL;

No need to explicitly initialize the loop variable.

+           RowExpr     *rowexpr = NULL;
+
+           if (!IsA(expr, RowExpr))
+               ereport(ERROR,
+

(errcode(ERRCODE_INVALID_TABLE_DEFINITION),

+ errmsg("Invalid list bound

specification"),

+ parser_errposition(pstate,

exprLocation((Node

*) spec))));
+
+           rowexpr = (RowExpr *) expr;

It's okay to assign rowexpr at the top here instead of the dummy
NULL-initialization and write the condition as:

if (!IsA(rowexpr, RowExpr))

+       if (isDuplicate)
+           continue;
+
+       result = lappend(result, values);

I can see you copied this style from the existing code, but how

about

writing this simply as:

if (!isDuplicate)
result = lappend(result, values);

-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
typedef struct PartitionListValue
{
int         index;
-   Datum       value;
+   Datum      *values;
+   bool       *isnulls;
} PartitionListValue;

Given that this is a locally-defined struct, I wonder if it makes
sense to rename the struct while we're at it. Call it, say,
PartitionListBound?

Also, please keep part of the existing comment that says that the
bound belongs to index'th partition.

Will send more comments in a bit...

+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
*/

I suggest slight rewording, as follows:

"Returns TRUE if any of the partition bounds contains a NULL value,
FALSE otherwise."

-   PartitionListValue *all_values;
+   PartitionListValue **all_values;
...
-   all_values = (PartitionListValue *)
-       palloc(ndatums * sizeof(PartitionListValue));
+   ndatums = get_list_datum_count(boundspecs, nparts);
+   all_values = (PartitionListValue **)
+       palloc(ndatums * sizeof(PartitionListValue *));

I don't see the need to redefine all_values's pointer type. No need
to palloc PartitionListValue repeatedly for every datum as done
further down as follows:

+ all_values[j] = (PartitionListValue *)
palloc(sizeof(PartitionListValue));

You do need the following two though:

+ all_values[j]->values = (Datum *)

palloc0(key->partnatts *

sizeof(Datum));
+ all_values[j]->isnulls = (bool *)

palloc0(key->partnatts *

sizeof(bool));

If you change the above the way I suggest, you'd also need to revert
the following change:

-   qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+   qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
qsort_partition_list_value_cmp, (void *) key);
+       int         orig_index = all_values[i]->index;
+       boundinfo->datums[i] = (Datum *) palloc(key->partnatts *

sizeof(Datum));

Missing a newline between these two statements.

BTW, I noticed that the boundDatums variable is no longer used in
create_list_bounds. I traced back its origin and found that a

recent

commit 53d86957e98 introduced it to implement an idea to reduce the
finer-grained pallocs that were being done in

create_list_bounds(). I

don't think that this patch needs to throw away that work. You can
make it work as the attached delta patch that applies on top of v3.
Please check.

@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16
*parttyplen, bool *parttypbyval,
if (b1->nindexes != b2->nindexes)
return false;

-   if (b1->null_index != b2->null_index)
+   if (get_partition_bound_null_index(b1) !=
get_partition_bound_null_index(b2))

As mentioned in the last message, this bit in

partition_bounds_equal()

needs to be comparing "bitmapsets" of null bound indexes, that is
after fixing get_partition_bound_null_index() as previously

mentioned.

But...

@@ -988,7 +1022,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;
+               }

...if you have this in the main loop, I don't think we need the

above

code stanza which appears to implement a short-cut for this

long-form

logic.

+               (key->strategy != PARTITION_STRATEGY_LIST ||
+                !src->isnulls[i][j]))

I think it's better to write this condition as follows just like the
accompanying condition involving src->kind:

(src->nulls == NULL || !src->isnulls[i][j])

(Skipped looking at merge_list_bounds() and related changes for now

as

I see a lot of TODOs remain to be done.)

In check_new_partition_bound():

+                       Datum      *values = (Datum *)
palloc0(key->partnatts * sizeof(Datum));
+                       bool       *isnulls = (bool *)
palloc0(key->partnatts * sizeof(bool));

Doesn't seem like a bad idea to declare these as:

Datum values[PARTITION_MAX_KEYS];
bool isnulls[PARTITION_MAX_KEYS];

I looked at get_qual_for_list_multi_column() and immediately thought
that it may be a bad idea. I think it's better to integrate the

logic

for multi-column case into the existing function even if that makes
the function appear more complex. Having two functions with the

same

goal and mostly the same code is not a good idea mainly because it
becomes a maintenance burden.

I have attempted a rewrite such that get_qual_for_list() now handles
both the single-column and multi-column cases. Changes included in
the delta patch. The patch updates some outputs of the newly added
tests for multi-column list partitions, because the new code emits

the

IS NOT NULL tests a bit differently than
get_qual_for_list_mutli_column() would. Notably, the old approach
would emit IS NOT NULL for every non-NULL datum matched to a given
column, not just once for the column. However, the patch makes a

few

other tests fail, mainly because I had to fix
partition_bound_accepts_nulls() to handle the multi-column case,
though didn't bother to update all callers of it to also handle the
multi-column case correctly. I guess that's a TODO you're going to
deal with at some point anyway. :)

I still have more than half of v3 left to look at, so will continue
looking. In the meantime, please check the changes I suggested,
including the delta patch, and let me know your thoughts.

--
Amit Langote
EDB: http://www.enterprisedb.com

Attachments:

v6-0001-multi-column-list-partitioning.patchapplication/octet-stream; name=v6-0001-multi-column-list-partitioning.patchDownload
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

#30Zhihong Yu
zyu@yugabyte.com
In reply to: Nitin Jadhav (#29)
Re: Multi-Column List Partitioning

On Fri, Oct 22, 2021 at 2:48 AM Nitin Jadhav <nitinjadhavpostgres@gmail.com>
wrote:

While testing further I got a crash with partition wise join enabled for

multi-col list partitions. please find test case & stack-trace below.

Thanks for sharing. I have fixed the issue in the attached patch.

Thanks & Regards,
Nitin Jadhav

Hi,

+isListBoundDuplicated(List *list_bounds, List *new_bound)

+           Const   *value1 = castNode(Const, list_nth(elem, i));
+           Const   *value2 = castNode(Const, list_nth(new_bound, i));

Should the upper bound for index i take into account the length of
new_bound ?
If the length of new_bound is always the same as that for elem, please add
an assertion.

For transformPartitionListBounds():
+               deparse_expression((Node *) list_nth(partexprs, j),
+
 deparse_context_for(RelationGetRelationName(parent),
+
 RelationGetRelid(parent)),

Please consider calling RelationGetRelationName(parent)
and RelationGetRelid(parent) (and assigning to local variables) outside the
loop.

+get_list_datum_count(PartitionBoundSpec **boundspecs, int nparts)

get_list_datum_count -> get_list_datums_count

For partition_bounds_equal():

+               if (b1->isnulls)
+                   b1_isnull = b1->isnulls[i][j];
+               if (b2->isnulls)
+                   b2_isnull = b2->isnulls[i][j];

Should the initialization of b1_isnull and b2_isnull be done inside the
loop (so that they don't inherit value from previous iteration) ?

Cheers

#31Zhihong Yu
zyu@yugabyte.com
In reply to: Zhihong Yu (#30)
Re: Multi-Column List Partitioning

On Fri, Oct 22, 2021 at 3:50 AM Zhihong Yu <zyu@yugabyte.com> wrote:

On Fri, Oct 22, 2021 at 2:48 AM Nitin Jadhav <
nitinjadhavpostgres@gmail.com> wrote:

While testing further I got a crash with partition wise join enabled

for multi-col list partitions. please find test case & stack-trace below.

Thanks for sharing. I have fixed the issue in the attached patch.

Thanks & Regards,
Nitin Jadhav

Hi,

+isListBoundDuplicated(List *list_bounds, List *new_bound)

+           Const   *value1 = castNode(Const, list_nth(elem, i));
+           Const   *value2 = castNode(Const, list_nth(new_bound, i));

Should the upper bound for index i take into account the length of
new_bound ?
If the length of new_bound is always the same as that for elem, please add
an assertion.

For transformPartitionListBounds():
+               deparse_expression((Node *) list_nth(partexprs, j),
+
deparse_context_for(RelationGetRelationName(parent),
+
RelationGetRelid(parent)),

Please consider calling RelationGetRelationName(parent)
and RelationGetRelid(parent) (and assigning to local variables) outside the
loop.

+get_list_datum_count(PartitionBoundSpec **boundspecs, int nparts)

get_list_datum_count -> get_list_datums_count

For partition_bounds_equal():

+               if (b1->isnulls)
+                   b1_isnull = b1->isnulls[i][j];
+               if (b2->isnulls)
+                   b2_isnull = b2->isnulls[i][j];

Should the initialization of b1_isnull and b2_isnull be done inside the
loop (so that they don't inherit value from previous iteration) ?

Cheers

Hi,
Continuing review.

+ * For the multi-column case, we must make an BoolExpr that

an BoolExpr -> a BoolExpr

In get_qual_for_list(), it would be better if repetitive code can be
extracted into a helper method:

+               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;
+               }

For match_clause_to_partition_key():

+       if (part_scheme->strategy != PARTITION_STRATEGY_LIST)
+       {
+           *clause_is_not_null = (nulltest->nulltesttype == IS_NOT_NULL);
+           return PARTCLAUSE_MATCH_NULLNESS;
+       }
+       else

Since the if block ends with return, the 'else' is not needed - else block
can be indented to the left.

get_min_and_max_off(): I think get_min_and_max_offset as method name would
be more informative.

+   Assert(0 == partition_lbound_datum_cmp(partsupfunc, partcollation,
+                                          boundinfo->datums[off],
+                                          boundinfo->isnulls[off],
+                                          values, isnulls, nvalues));

If the 'while (off >= 1)' loop exits without modifying off, is the above
assertion always true (can boundinfo->datums[off] be accessed without
checking bound) ?

Cheers

#32Amit Langote
amitlangote09@gmail.com
In reply to: Nitin Jadhav (#29)
Re: Multi-Column List Partitioning

Hi Nitin,

On Fri, Oct 22, 2021 at 6:48 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

Thanks for sharing. I have fixed the issue in the attached patch.

I noticed that there's no commitfest entry for this. Will you please
add this to the next one?

--
Amit Langote
EDB: http://www.enterprisedb.com

#33Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Zhihong Yu (#31)
1 attachment(s)
Re: Multi-Column List Partitioning

Thanks for reviewing.

+isListBoundDuplicated(List *list_bounds, List *new_bound)

+           Const   *value1 = castNode(Const, list_nth(elem, i));
+           Const   *value2 = castNode(Const, list_nth(new_bound, i));

Should the upper bound for index i take into account the length of new_bound ?
If the length of new_bound is always the same as that for elem, please add an assertion.

The length of 'elem' should be same as length of 'new_bound'. Added
assert statement for the same.

For transformPartitionListBounds():
+               deparse_expression((Node *) list_nth(partexprs, j),
+                                  deparse_context_for(RelationGetRelationName(parent),
+                                                      RelationGetRelid(parent)),

Please consider calling RelationGetRelationName(parent) and RelationGetRelid(parent) (and assigning to local variables) outside the loop.

I don't think this is an issue as 'RelationGetRelationName' and
'RelationGetRelid' are macros. Please let me know if your opinion is
different.

+get_list_datum_count(PartitionBoundSpec **boundspecs, int nparts)

get_list_datum_count -> get_list_datums_count

There was a function earlier with the name
'get_non_null_list_datum_count()'. So now this has changed to
'get_list_datum_count()' as we are not separating the non null datums
from the list. The new name is inline with the old function name which
was already accepted by the community. So I feel it is better to not
change.

For partition_bounds_equal():

+               if (b1->isnulls)
+                   b1_isnull = b1->isnulls[i][j];
+               if (b2->isnulls)
+                   b2_isnull = b2->isnulls[i][j];

Should the initialization of b1_isnull and b2_isnull be done inside the loop (so that they don't inherit value from previous iteration) ?

Nice catch. Fixed.

In get_qual_for_list(), it would be better if repetitive code can be extracted into a helper method:

I have removed the repetitive code and made a common function named
'get_qual_for_list_datums()'.

For match_clause_to_partition_key():

+       if (part_scheme->strategy != PARTITION_STRATEGY_LIST)
+       {
+           *clause_is_not_null = (nulltest->nulltesttype == IS_NOT_NULL);
+           return PARTCLAUSE_MATCH_NULLNESS;
+       }
+       else

Since the if block ends with return, the 'else' is not needed - else block can be indented to the left.

Fixed.

get_min_and_max_off(): I think get_min_and_max_offset as method name would be more informative.

Fixed.

+   Assert(0 == partition_lbound_datum_cmp(partsupfunc, partcollation,
+                                          boundinfo->datums[off],
+                                          boundinfo->isnulls[off],
+                                          values, isnulls, nvalues));

If the 'while (off >= 1)' loop exits without modifying off, is the above assertion always true (can boundinfo->datums[off] be accessed without checking bound) ?

Yes. The assertion holds good even though the control doesn't enter
the loop. In that case the 'off' can be directly considered as minoff
or maxoff. Since we are considering it as valid, the assertion is
needed.

Thanks & Regards,
Nitin Jadhav

Show quoted text

On Fri, Oct 22, 2021 at 9:30 PM Zhihong Yu <zyu@yugabyte.com> wrote:

On Fri, Oct 22, 2021 at 3:50 AM Zhihong Yu <zyu@yugabyte.com> wrote:

On Fri, Oct 22, 2021 at 2:48 AM Nitin Jadhav <nitinjadhavpostgres@gmail.com> wrote:

While testing further I got a crash with partition wise join enabled for multi-col list partitions. please find test case & stack-trace below.

Thanks for sharing. I have fixed the issue in the attached patch.

Thanks & Regards,
Nitin Jadhav

Hi,

+isListBoundDuplicated(List *list_bounds, List *new_bound)

+           Const   *value1 = castNode(Const, list_nth(elem, i));
+           Const   *value2 = castNode(Const, list_nth(new_bound, i));

Should the upper bound for index i take into account the length of new_bound ?
If the length of new_bound is always the same as that for elem, please add an assertion.

For transformPartitionListBounds():
+               deparse_expression((Node *) list_nth(partexprs, j),
+                                  deparse_context_for(RelationGetRelationName(parent),
+                                                      RelationGetRelid(parent)),

Please consider calling RelationGetRelationName(parent) and RelationGetRelid(parent) (and assigning to local variables) outside the loop.

+get_list_datum_count(PartitionBoundSpec **boundspecs, int nparts)

get_list_datum_count -> get_list_datums_count

For partition_bounds_equal():

+               if (b1->isnulls)
+                   b1_isnull = b1->isnulls[i][j];
+               if (b2->isnulls)
+                   b2_isnull = b2->isnulls[i][j];

Should the initialization of b1_isnull and b2_isnull be done inside the loop (so that they don't inherit value from previous iteration) ?

Cheers

Hi,
Continuing review.

+ * For the multi-column case, we must make an BoolExpr that

an BoolExpr -> a BoolExpr

In get_qual_for_list(), it would be better if repetitive code can be extracted into a helper method:

+               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;
+               }

For match_clause_to_partition_key():

+       if (part_scheme->strategy != PARTITION_STRATEGY_LIST)
+       {
+           *clause_is_not_null = (nulltest->nulltesttype == IS_NOT_NULL);
+           return PARTCLAUSE_MATCH_NULLNESS;
+       }
+       else

Since the if block ends with return, the 'else' is not needed - else block can be indented to the left.

get_min_and_max_off(): I think get_min_and_max_offset as method name would be more informative.

+   Assert(0 == partition_lbound_datum_cmp(partsupfunc, partcollation,
+                                          boundinfo->datums[off],
+                                          boundinfo->isnulls[off],
+                                          values, isnulls, nvalues));

If the 'while (off >= 1)' loop exits without modifying off, is the above assertion always true (can boundinfo->datums[off] be accessed without checking bound) ?

Cheers

Attachments:

v7-0001-multi-column-list-partitioning.patchapplication/octet-stream; name=v7-0001-multi-column-list-partitioning.patchDownload
From 930590ca3f8087ad989fd06ffdbb0a3d61755df3 Mon Sep 17 00:00:00 2001
From: Nitin <nitin.jadhav@enterprisedb.com>
Date: Tue, 2 Nov 2021 17:38:45 +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            |  194 +++-
 src/backend/partitioning/partbounds.c         |  896 +++++++++++-------
 src/backend/partitioning/partprune.c          |  462 ++++++---
 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, 3364 insertions(+), 559 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 857cc5c..b6dd563 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16762,13 +16762,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..7aacc81 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,44 @@ 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;
+
+		Assert(list_length(elem) == list_length(new_bound));
+
+		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 +4037,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 +4086,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 +4130,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..2889c2f 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;
 
@@ -915,9 +926,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;
 
@@ -960,6 +968,9 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 
 			for (j = 0; j < partnatts; j++)
 			{
+				bool        b1_isnull = false;
+				bool        b2_isnull = false;
+
 				/* For range partitions, the bounds might not be finite. */
 				if (b1->kind != NULL)
 				{
@@ -988,7 +999,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 +1052,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 +1102,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 +1121,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 +1196,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 +1241,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 +1254,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 +1263,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 +1301,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 +1349,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 +1385,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 +1399,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 +1437,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 +1475,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 +1489,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 +1527,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 +1544,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 +1581,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 +1592,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 +1901,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 +2260,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 +2289,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 +2304,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 +2327,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 +2606,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 +2617,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 +2645,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 +2657,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 +3164,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 +3703,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 +3754,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 +3768,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 +3941,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 +4045,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 +4075,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 +4095,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;
 			}
@@ -4070,6 +4212,106 @@ get_qual_for_hash(Relation parent, PartitionBoundSpec *spec)
 }
 
 /*
+ * get_qual_for_list_datums
+ *
+ * Returns an implicit-AND list of expressions to use as a list partition's
+ * constraint, given the partition bound structure.
+ */
+static List *
+get_qual_for_list_datums(PartitionKey key, PartitionBoundInfo bound_info,
+						 List *list_datums, Expr **key_col, bool is_default,
+						 bool *key_is_null, Expr **is_null_test)
+{
+	int 		i;
+	int			j;
+	int			ndatums;
+	bool		is_null;
+	List	   *datum_elems = NIL;
+
+	if (is_default)
+		ndatums = bound_info->ndatums;
+	else
+		ndatums = list_length(list_datums);
+
+	for (i = 0; i < ndatums; i++)
+	{
+		List       *and_args = NIL;
+		Expr       *datum_elem = NULL;
+
+		/*
+		 * 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.
+		 */
+		for (j = 0; j < key->partnatts; j++)
+		{
+			Const      *val = NULL;
+
+			if (is_default)
+				is_null = bound_info->isnulls[i][j];
+			else
+			{
+				List   *listbound = list_nth(list_datums, i);
+
+				val = castNode(Const, list_nth(listbound, j));
+				is_null = val->constisnull;
+			}
+
+			if (is_null)
+			{
+				NullTest   *nulltest = makeNode(NullTest);
+
+				nulltest->arg = key_col[j];
+				nulltest->nulltesttype = IS_NULL;
+				nulltest->argisrow = false;
+				nulltest->location = -1;
+				key_is_null[j] = true;
+
+				if (key->partnatts > 1)
+					and_args = lappend(and_args, nulltest);
+				else
+					*is_null_test = (Expr *) nulltest;
+			}
+			else
+			{
+				if (is_default)
+				{
+					val = makeConst(key->parttypid[j],
+								key->parttypmod[j],
+								key->parttypcoll[j],
+								key->parttyplen[j],
+								datumCopy(bound_info->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,
+														  key_col[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);
+	}
+
+	return datum_elems;
+}
+
+/*
  * get_qual_for_list
  *
  * Returns an implicit-AND list of expressions to use as a list partition's
@@ -4082,30 +4324,40 @@ static List *
 get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 {
 	PartitionKey key = RelationGetPartitionKey(parent);
-	List	   *result;
-	Expr	   *keyCol;
-	Expr	   *opexpr;
-	NullTest   *nulltest;
-	ListCell   *cell;
-	List	   *elems = NIL;
-	bool		list_has_null = false;
+	List	   *result = NIL;
+	Expr	   *datumtest;
+	Expr	   *is_null_test = NULL;
+	List	   *datum_elems = NIL;
+	bool		key_is_null[PARTITION_MAX_KEYS];
+	int			i,
+				j;
+	Expr      **keyCol = (Expr **) palloc0 (key->partnatts * sizeof(Expr *));
+	PartitionBoundInfo boundinfo;
 
-	/*
-	 * 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
@@ -4114,119 +4366,83 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 	 */
 	if (spec->is_default)
 	{
-		int			i;
 		int			ndatums = 0;
 		PartitionDesc pdesc = RelationGetPartitionDesc(parent, false);
-		PartitionBoundInfo boundinfo = pdesc->boundinfo;
+		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;
-
-			/*
-			 * 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.
-			 */
-			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);
-		}
 	}
-	else
+
+	datum_elems = get_qual_for_list_datums(key, boundinfo, spec->listdatums,
+										   keyCol, spec->is_default, key_is_null,
+										   &is_null_test);
+
+	/*
+	 * 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++)
 	{
-		/*
-		 * Create list of Consts for the allowed values, excluding any nulls.
-		 */
-		foreach(cell, spec->listdatums)
+		if (!key_is_null[i])
 		{
-			Const	   *val = lfirst_node(Const, cell);
-
-			if (val->constisnull)
-				list_has_null = true;
-			else
-				elems = lappend(elems, copyObject(val));
+			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 (elems)
+	/*
+	 * 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)
 	{
-		/*
-		 * Generate the operator expression from the non-null partition
-		 * values.
-		 */
-		opexpr = make_partition_op_expr(key, 0, BTEqualStrategyNumber,
-										keyCol, (Expr *) elems);
+		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
-	{
-		/*
-		 * If there are no partition values, we don't need an operator
-		 * expression.
-		 */
-		opexpr = NULL;
-	}
-
-	if (!list_has_null)
-	{
-		/*
-		 * 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;
+		datumtest = NULL;
 
-		result = opexpr ? list_make2(nulltest, opexpr) : list_make1(nulltest);
-	}
-	else
+	/*
+	 * 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)
 	{
-		/*
-		 * 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;
+		Expr *orexpr = makeBoolExpr(OR_EXPR,
+									list_make2(is_null_test, datumtest),
+									-1);
 
-		if (opexpr)
-		{
-			Expr	   *or;
-
-			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..d7bbee9 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,
@@ -2316,6 +2314,8 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 	{
 		NullTest   *nulltest = (NullTest *) clause;
 		Expr	   *arg = nulltest->arg;
+		Const	   *expr;
+		PartClauseInfo *partclause;
 
 		if (IsA(arg, RelabelType))
 			arg = ((RelabelType *) arg)->arg;
@@ -2324,9 +2324,32 @@ 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;
+		}
+
+		expr = makeConst(UNKNOWNOID, -1, InvalidOid, -2, (Datum) 0, true, false);
+		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 +2650,170 @@ get_matching_hash_bounds(PartitionPruneContext *context,
 											  boundinfo->nindexes - 1);
 	}
 
+	return result;
+}
+
+/*
+ * get_min_and_max_offset
+ *
+ * Fetches the minimum and maximum offset of the matching partitions.
+ */
+static void
+get_min_and_max_offset(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 +2822,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 +2837,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 +2848,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 +2863,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 +2872,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 +2885,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;
+
+				/*
+				 * 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_offset(context, partsupfunc, values, isnulls,
+									   nvalues, off, &minoff, &maxoff);
 
-			/* We have a match. Remove from the result. */
-			Assert(boundinfo->indexes[off] >= 0);
-			result->bound_offsets = bms_del_member(result->bound_offsets,
-												   off);
+				/* 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 +2937,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_offset(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 +3001,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 +3030,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 +3082,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 +3537,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 +3559,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 +3586,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 +3657,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 +3706,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 +3732,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 +3755,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 +3765,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

#34Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Amit Langote (#32)
Re: Multi-Column List Partitioning

I noticed that there's no commitfest entry for this. Will you please
add this to the next one?

I have added it to Nov commitfest.

Thanks & Regards,
Nitin Jadhav

Show quoted text

On Fri, Oct 29, 2021 at 1:40 PM Amit Langote <amitlangote09@gmail.com> wrote:

Hi Nitin,

On Fri, Oct 22, 2021 at 6:48 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

Thanks for sharing. I have fixed the issue in the attached patch.

I noticed that there's no commitfest entry for this. Will you please
add this to the next one?

--
Amit Langote
EDB: http://www.enterprisedb.com

#35Amul Sul
sulamul@gmail.com
In reply to: Nitin Jadhav (#34)
Re: Multi-Column List Partitioning

Hi,

Few comments for v7 patch, note that I haven't been through the
previous discussion, if any of the review comments that has been
already discussed & overridden, then please ignore here too:

partbounds.c: In function ‘get_qual_for_list.isra.18’:
partbounds.c:4284:29: warning: ‘boundinfo’ may be used uninitialized
in this function [-Wmaybe-uninitialized]
datumCopy(bound_info->datums[i][j],
~~~~~~~~~~^~~~~~~~
partbounds.c:4335:21: note: ‘boundinfo’ was declared here
PartitionBoundInfo boundinfo;
^~~~~~~~~
partbounds.c: In function ‘partition_bounds_merge’:
partbounds.c:1305:12: warning: ‘inner_isnull’ may be used
uninitialized in this function [-Wmaybe-uninitialized]
bool *inner_isnull;
^~~~~~~~~~~~
partbounds.c:1304:12: warning: ‘outer_isnull’ may be used
uninitialized in this function [-Wmaybe-uninitialized]
bool *outer_isnull;
^~~~~~~~~~~~

Got these warnings with gcc -O2 compilation.
----

 /*
+ * 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;
+
+ Assert(list_length(elem) == list_length(new_bound));
+
+ 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;
+}

This function is unnecessarily complicated, I think you can avoid
inner for loops; simply replace for-loop-block with "if
(equal(lfirst(cell), new_bound)) return true".
----

+ 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));
+
This allocation seems to be worthless, read ahead.
----
+ 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);
+ }

I think there is no need for this separate loop inside
transformPartitionListBounds, you can do that same in the next loop as
well. And instead of get_partition_col_* calling and storing, simply
use that directly as an argument to transformPartitionBoundValue().
----

+
+ 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))));
+

I think this should be inside the "else" block after "!IsA(rowexpr,
RowExpr)" error and you can avoid IsA() check too.
----

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

Looks difficult to understand at first glance, how about the following:

if (b1->isnulls != b2->isnulls)
return false;

if (b1->isnulls)
{
if (b1->isnulls[i][j] != b2->isnulls[i][j])
return false;
if (b1->isnulls[i][j])
continue;
}

See how range partitioning infinite values are handled. Also, place
this before the comment block that was added for the "!datumIsEqual()"
case.
----

+       if (src->isnulls)
+           dest->isnulls[i] = (bool *) palloc(sizeof(bool) * natts);
...
+           if (src->isnulls)
+               dest->isnulls[i][j] = src->isnulls[i][j];
+
Nothing wrong with this but if we could have checked "dest->isnulls"
instead of "src->isnulls" would be much better.
----
-           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);
Condition "key->strategy != PARTITION_STRATEGY_LIST" seems to be unnecessary.
----
+       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;
+           }
+       }

Can't be a single loop?
----

It would be helpful if you could run pgindent on your patch if not done already.
----

That's all for now, I am yet to finish the complete patch reading and
understand the code flow, but I am out of time now.

Regards,
Amul

#36Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Amul Sul (#35)
1 attachment(s)
Re: Multi-Column List Partitioning

Thank you for reviewing the patch.

partbounds.c: In function ‘get_qual_for_list.isra.18’:
partbounds.c:4284:29: warning: ‘boundinfo’ may be used uninitialized
in this function [-Wmaybe-uninitialized]
datumCopy(bound_info->datums[i][j],
~~~~~~~~~~^~~~~~~~
partbounds.c:4335:21: note: ‘boundinfo’ was declared here
PartitionBoundInfo boundinfo;
^~~~~~~~~
partbounds.c: In function ‘partition_bounds_merge’:
partbounds.c:1305:12: warning: ‘inner_isnull’ may be used
uninitialized in this function [-Wmaybe-uninitialized]
bool *inner_isnull;
^~~~~~~~~~~~
partbounds.c:1304:12: warning: ‘outer_isnull’ may be used
uninitialized in this function [-Wmaybe-uninitialized]
bool *outer_isnull;
^~~~~~~~~~~~

Fixed.

This function is unnecessarily complicated, I think you can avoid
inner for loops; simply replace for-loop-block with "if
(equal(lfirst(cell), new_bound)) return true".

Thank you for the suggestion. Fixed.

+ 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));
+
This allocation seems to be worthless, read ahead.

I think there is no need for this separate loop inside
transformPartitionListBounds, you can do that same in the next loop as
well. And instead of get_partition_col_* calling and storing, simply
use that directly as an argument to transformPartitionBoundValue().

Yes. The loop can be avoided and content of the above loop can be
included in the next loop but the next loop iterates over a list of
multi column datums. For each iteration, we need the information of
all the columns. The above data (colname, coltype, coltypmod and
partcollation) remains same for each iteration of the loop, If we
modify as suggested, then the function to fetch these information has
to be called every-time. To avoid this situation I have made a
separate loop outside which only runs as many number of columns and
stores in a variable which can be reused later. Please let me correct
if I am wrong.

I think this should be inside the "else" block after "!IsA(rowexpr,
RowExpr)" error and you can avoid IsA() check too.

This is required to handle the situation when one partition key is
mentioned and multiple values are provided in the partition bound
specification.

Looks difficult to understand at first glance, how about the following:

if (b1->isnulls != b2->isnulls)
return false;

if (b1->isnulls)
{
if (b1->isnulls[i][j] != b2->isnulls[i][j])
return false;
if (b1->isnulls[i][j])
continue;
}

See how range partitioning infinite values are handled. Also, place
this before the comment block that was added for the "!datumIsEqual()"
case.

Fixed. I feel the 'continue' block is not required and hence removed it.

Nothing wrong with this but if we could have checked "dest->isnulls"
instead of "src->isnulls" would be much better.

Here we are copying the data from 'src' to 'dest'. If there is no data
in 'src', it is unnecessary to copy. Hence checking 'src'.

Condition "key->strategy != PARTITION_STRATEGY_LIST" seems to be unnecessary.

Fixed.

Can't be a single loop?

Yes. Fixed.

Show quoted text

On Fri, Dec 3, 2021 at 7:26 PM Amul Sul <sulamul@gmail.com> wrote:

Hi,

Few comments for v7 patch, note that I haven't been through the
previous discussion, if any of the review comments that has been
already discussed & overridden, then please ignore here too:

partbounds.c: In function ‘get_qual_for_list.isra.18’:
partbounds.c:4284:29: warning: ‘boundinfo’ may be used uninitialized
in this function [-Wmaybe-uninitialized]
datumCopy(bound_info->datums[i][j],
~~~~~~~~~~^~~~~~~~
partbounds.c:4335:21: note: ‘boundinfo’ was declared here
PartitionBoundInfo boundinfo;
^~~~~~~~~
partbounds.c: In function ‘partition_bounds_merge’:
partbounds.c:1305:12: warning: ‘inner_isnull’ may be used
uninitialized in this function [-Wmaybe-uninitialized]
bool *inner_isnull;
^~~~~~~~~~~~
partbounds.c:1304:12: warning: ‘outer_isnull’ may be used
uninitialized in this function [-Wmaybe-uninitialized]
bool *outer_isnull;
^~~~~~~~~~~~

Got these warnings with gcc -O2 compilation.
----

/*
+ * 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;
+
+ Assert(list_length(elem) == list_length(new_bound));
+
+ 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;
+}

This function is unnecessarily complicated, I think you can avoid
inner for loops; simply replace for-loop-block with "if
(equal(lfirst(cell), new_bound)) return true".
----

+ 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));
+
This allocation seems to be worthless, read ahead.
----
+ 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);
+ }

I think there is no need for this separate loop inside
transformPartitionListBounds, you can do that same in the next loop as
well. And instead of get_partition_col_* calling and storing, simply
use that directly as an argument to transformPartitionBoundValue().
----

+
+ 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))));
+

I think this should be inside the "else" block after "!IsA(rowexpr,
RowExpr)" error and you can avoid IsA() check too.
----

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

Looks difficult to understand at first glance, how about the following:

if (b1->isnulls != b2->isnulls)
return false;

if (b1->isnulls)
{
if (b1->isnulls[i][j] != b2->isnulls[i][j])
return false;
if (b1->isnulls[i][j])
continue;
}

See how range partitioning infinite values are handled. Also, place
this before the comment block that was added for the "!datumIsEqual()"
case.
----

+       if (src->isnulls)
+           dest->isnulls[i] = (bool *) palloc(sizeof(bool) * natts);
...
+           if (src->isnulls)
+               dest->isnulls[i][j] = src->isnulls[i][j];
+
Nothing wrong with this but if we could have checked "dest->isnulls"
instead of "src->isnulls" would be much better.
----
-           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);
Condition "key->strategy != PARTITION_STRATEGY_LIST" seems to be unnecessary.
----
+       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;
+           }
+       }

Can't be a single loop?
----

It would be helpful if you could run pgindent on your patch if not done already.
----

That's all for now, I am yet to finish the complete patch reading and
understand the code flow, but I am out of time now.

Regards,
Amul

Attachments:

v8-0001-multi-column-list-partitioning.patchapplication/x-patch; name=v8-0001-multi-column-list-partitioning.patchDownload
From 1574bdb2457c2bc779c7ac333d19edff73e105c0 Mon Sep 17 00:00:00 2001
From: Nitin <nitin.jadhav@enterprisedb.com>
Date: Mon, 6 Dec 2021 18:03:11 +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            |  176 +++-
 src/backend/partitioning/partbounds.c         |  890 ++++++++++-------
 src/backend/partitioning/partprune.c          |  462 ++++++---
 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, 3340 insertions(+), 559 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c821271..7c92e77 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16778,13 +16778,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..61a7041 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,26 @@ 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)
+	{
+		if (equal(lfirst(cell), new_bound))
+			return true;
+	}
+
+	return false;
+}
+
+/*
  * transformPartitionBound
  *
  * Transform a partition bound specification
@@ -3996,7 +4019,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 +4068,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 +4112,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..c8ad4a6 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;
 
@@ -915,9 +926,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;
 
@@ -960,6 +968,9 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 
 			for (j = 0; j < partnatts; j++)
 			{
+				bool        b1_isnull = false;
+				bool        b2_isnull = false;
+
 				/* For range partitions, the bounds might not be finite. */
 				if (b1->kind != NULL)
 				{
@@ -975,6 +986,19 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 						continue;
 				}
 
+				/* 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->isnulls != b2->isnulls)
+					return false;
+
+				if (b1->isnulls)
+				{
+					if (b1->isnulls[i][j] != b2->isnulls[i][j])
+						return false;
+				}
+
 				/*
 				 * Compare the actual values. Note that it would be both
 				 * incorrect and unsafe to invoke the comparison operator
@@ -988,7 +1012,7 @@ 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],
+				else if (!datumIsEqual(b1->datums[i][j], b2->datums[i][j],
 								  parttypbyval[j], parttyplen[j]))
 					return false;
 			}
@@ -1026,10 +1050,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 +1100,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 +1119,21 @@ 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) &&
+				(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 +1193,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 +1238,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 +1251,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 +1260,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 +1298,20 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		int			cmpval;
 		Datum	   *merged_datum = NULL;
 		int			merged_index = -1;
+		bool	   *outer_isnull = NULL;
+		bool	   *inner_isnull = NULL;
+		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 +1346,23 @@ 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 && outer_isnull[i])
+			{
+				outer_has_null = true;
+				if (outer_map.merged_indexes[outer_index] == -1)
+					consider_outer_null = true;
+			}
+
+			if (inner_isnull && 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 +1379,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 +1393,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 +1431,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 +1469,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 +1483,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 +1521,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 +1538,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 +1575,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 +1586,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 +1895,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 +2254,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 +2283,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 +2298,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 +2321,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 +2600,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 +2611,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 +2639,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 +2651,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 +3158,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 +3697,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 = -1;
+
+	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 +3748,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 +3762,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 +3935,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 +4039,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 +4069,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 +4089,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;
 			}
@@ -4070,6 +4206,106 @@ get_qual_for_hash(Relation parent, PartitionBoundSpec *spec)
 }
 
 /*
+ * get_qual_for_list_datums
+ *
+ * Returns an implicit-AND list of expressions to use as a list partition's
+ * constraint, given the partition bound structure.
+ */
+static List *
+get_qual_for_list_datums(PartitionKey key, PartitionBoundInfo bound_info,
+						 List *list_datums, Expr **key_col, bool is_default,
+						 bool *key_is_null, Expr **is_null_test)
+{
+	int 		i;
+	int			j;
+	int			ndatums;
+	bool		is_null;
+	List	   *datum_elems = NIL;
+
+	if (is_default)
+		ndatums = bound_info->ndatums;
+	else
+		ndatums = list_length(list_datums);
+
+	for (i = 0; i < ndatums; i++)
+	{
+		List       *and_args = NIL;
+		Expr       *datum_elem = NULL;
+
+		/*
+		 * 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.
+		 */
+		for (j = 0; j < key->partnatts; j++)
+		{
+			Const      *val = NULL;
+
+			if (is_default)
+				is_null = bound_info->isnulls[i][j];
+			else
+			{
+				List   *listbound = list_nth(list_datums, i);
+
+				val = castNode(Const, list_nth(listbound, j));
+				is_null = val->constisnull;
+			}
+
+			if (is_null)
+			{
+				NullTest   *nulltest = makeNode(NullTest);
+
+				nulltest->arg = key_col[j];
+				nulltest->nulltesttype = IS_NULL;
+				nulltest->argisrow = false;
+				nulltest->location = -1;
+				key_is_null[j] = true;
+
+				if (key->partnatts > 1)
+					and_args = lappend(and_args, nulltest);
+				else
+					*is_null_test = (Expr *) nulltest;
+			}
+			else
+			{
+				if (is_default)
+				{
+					val = makeConst(key->parttypid[j],
+								key->parttypmod[j],
+								key->parttypcoll[j],
+								key->parttyplen[j],
+								datumCopy(bound_info->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,
+														  key_col[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);
+	}
+
+	return datum_elems;
+}
+
+/*
  * get_qual_for_list
  *
  * Returns an implicit-AND list of expressions to use as a list partition's
@@ -4082,30 +4318,40 @@ static List *
 get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 {
 	PartitionKey key = RelationGetPartitionKey(parent);
-	List	   *result;
-	Expr	   *keyCol;
-	Expr	   *opexpr;
-	NullTest   *nulltest;
-	ListCell   *cell;
-	List	   *elems = NIL;
-	bool		list_has_null = false;
+	List	   *result = NIL;
+	Expr	   *datumtest;
+	Expr	   *is_null_test = NULL;
+	List	   *datum_elems = NIL;
+	bool		key_is_null[PARTITION_MAX_KEYS];
+	int			i,
+				j;
+	Expr      **keyCol = (Expr **) palloc0 (key->partnatts * sizeof(Expr *));
+	PartitionBoundInfo boundinfo = {0};
 
-	/*
-	 * 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
@@ -4114,119 +4360,83 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 	 */
 	if (spec->is_default)
 	{
-		int			i;
 		int			ndatums = 0;
 		PartitionDesc pdesc = RelationGetPartitionDesc(parent, false);
-		PartitionBoundInfo boundinfo = pdesc->boundinfo;
+		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;
-
-			/*
-			 * 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.
-			 */
-			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);
-		}
 	}
-	else
+
+	datum_elems = get_qual_for_list_datums(key, boundinfo, spec->listdatums,
+										   keyCol, spec->is_default, key_is_null,
+										   &is_null_test);
+
+	/*
+	 * 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++)
 	{
-		/*
-		 * Create list of Consts for the allowed values, excluding any nulls.
-		 */
-		foreach(cell, spec->listdatums)
+		if (!key_is_null[i])
 		{
-			Const	   *val = lfirst_node(Const, cell);
-
-			if (val->constisnull)
-				list_has_null = true;
-			else
-				elems = lappend(elems, copyObject(val));
+			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 (elems)
+	/*
+	 * 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)
 	{
-		/*
-		 * Generate the operator expression from the non-null partition
-		 * values.
-		 */
-		opexpr = make_partition_op_expr(key, 0, BTEqualStrategyNumber,
-										keyCol, (Expr *) elems);
+		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
-	{
-		/*
-		 * If there are no partition values, we don't need an operator
-		 * expression.
-		 */
-		opexpr = NULL;
-	}
-
-	if (!list_has_null)
-	{
-		/*
-		 * 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;
+		datumtest = NULL;
 
-		result = opexpr ? list_make2(nulltest, opexpr) : list_make1(nulltest);
-	}
-	else
+	/*
+	 * 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)
 	{
-		/*
-		 * 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;
+		Expr *orexpr = makeBoolExpr(OR_EXPR,
+									list_make2(is_null_test, datumtest),
+									-1);
 
-		if (opexpr)
-		{
-			Expr	   *or;
-
-			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..d7bbee9 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,
@@ -2316,6 +2314,8 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 	{
 		NullTest   *nulltest = (NullTest *) clause;
 		Expr	   *arg = nulltest->arg;
+		Const	   *expr;
+		PartClauseInfo *partclause;
 
 		if (IsA(arg, RelabelType))
 			arg = ((RelabelType *) arg)->arg;
@@ -2324,9 +2324,32 @@ 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;
+		}
+
+		expr = makeConst(UNKNOWNOID, -1, InvalidOid, -2, (Datum) 0, true, false);
+		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 +2650,170 @@ get_matching_hash_bounds(PartitionPruneContext *context,
 											  boundinfo->nindexes - 1);
 	}
 
+	return result;
+}
+
+/*
+ * get_min_and_max_offset
+ *
+ * Fetches the minimum and maximum offset of the matching partitions.
+ */
+static void
+get_min_and_max_offset(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 +2822,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 +2837,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 +2848,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 +2863,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 +2872,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 +2885,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;
+
+				/*
+				 * 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_offset(context, partsupfunc, values, isnulls,
+									   nvalues, off, &minoff, &maxoff);
 
-			/* We have a match. Remove from the result. */
-			Assert(boundinfo->indexes[off] >= 0);
-			result->bound_offsets = bms_del_member(result->bound_offsets,
-												   off);
+				/* 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 +2937,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_offset(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 +3001,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 +3030,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 +3082,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 +3537,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 +3559,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 +3586,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 +3657,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 +3706,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 +3732,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 +3755,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 +3765,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 6b4022c..7ab3cf4 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9451,10 +9451,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 = ", ";
 						}
 
@@ -12015,6 +12014,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

#37Amul Sul
sulamul@gmail.com
In reply to: Nitin Jadhav (#36)
Re: Multi-Column List Partitioning

On Mon, Dec 6, 2021 at 7:27 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

Thank you for reviewing the patch.

partbounds.c: In function ‘get_qual_for_list.isra.18’:
partbounds.c:4284:29: warning: ‘boundinfo’ may be used uninitialized
in this function [-Wmaybe-uninitialized]
datumCopy(bound_info->datums[i][j],
~~~~~~~~~~^~~~~~~~
partbounds.c:4335:21: note: ‘boundinfo’ was declared here
PartitionBoundInfo boundinfo;
^~~~~~~~~
partbounds.c: In function ‘partition_bounds_merge’:
partbounds.c:1305:12: warning: ‘inner_isnull’ may be used
uninitialized in this function [-Wmaybe-uninitialized]
bool *inner_isnull;
^~~~~~~~~~~~
partbounds.c:1304:12: warning: ‘outer_isnull’ may be used
uninitialized in this function [-Wmaybe-uninitialized]
bool *outer_isnull;
^~~~~~~~~~~~

Fixed.

This function is unnecessarily complicated, I think you can avoid
inner for loops; simply replace for-loop-block with "if
(equal(lfirst(cell), new_bound)) return true".

Thank you for the suggestion. Fixed.

+ 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));
+
This allocation seems to be worthless, read ahead.

I think there is no need for this separate loop inside
transformPartitionListBounds, you can do that same in the next loop as
well. And instead of get_partition_col_* calling and storing, simply
use that directly as an argument to transformPartitionBoundValue().

Yes. The loop can be avoided and content of the above loop can be
included in the next loop but the next loop iterates over a list of
multi column datums. For each iteration, we need the information of
all the columns. The above data (colname, coltype, coltypmod and
partcollation) remains same for each iteration of the loop, If we
modify as suggested, then the function to fetch these information has
to be called every-time. To avoid this situation I have made a
separate loop outside which only runs as many number of columns and
stores in a variable which can be reused later. Please let me correct
if I am wrong.

Ok, colname can be fetched in advance but I don't think it worth it to
fetch coltype, coltypmod & partcollation; and, store in the
explicitly allocated memory, instead, you can directly call
get_partition_col_* inline functions.

I think this should be inside the "else" block after "!IsA(rowexpr,
RowExpr)" error and you can avoid IsA() check too.

This is required to handle the situation when one partition key is
mentioned and multiple values are provided in the partition bound
specification.

Looks difficult to understand at first glance, how about the following:

if (b1->isnulls != b2->isnulls)
return false;

if (b1->isnulls)
{
if (b1->isnulls[i][j] != b2->isnulls[i][j])
return false;
if (b1->isnulls[i][j])
continue;
}

See how range partitioning infinite values are handled. Also, place
this before the comment block that was added for the "!datumIsEqual()"
case.

Fixed. I feel the 'continue' block is not required and hence removed it.

Nothing wrong with this but if we could have checked "dest->isnulls"
instead of "src->isnulls" would be much better.

Here we are copying the data from 'src' to 'dest'. If there is no data
in 'src', it is unnecessary to copy. Hence checking 'src'.

I am not sure how that makes a difference since you do allocate 'dest'
based on 'src'; anyway, I leave that choice to you.

Condition "key->strategy != PARTITION_STRATEGY_LIST" seems to be unnecessary.

Fixed.

Can't be a single loop?

Yes. Fixed.

Thanks, will have a look.

Regards,
Amul

#38Amit Langote
amitlangote09@gmail.com
In reply to: Nitin Jadhav (#36)
Re: Multi-Column List Partitioning

Hi Nitin,

Was looking at warnings generated by v8:

partbounds.c:971:17: warning: unused variable 'b1_isnull' [-Wunused-variable]
bool b1_isnull = false;
^
partbounds.c:972:17: warning: unused variable 'b2_isnull' [-Wunused-variable]
bool b2_isnull = false;

And it seems they've resulted from the above change:

On Mon, Dec 6, 2021 at 10:57 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

Looks difficult to understand at first glance, how about the following:

if (b1->isnulls != b2->isnulls)
return false;

I don't think having this block is correct, because this says that two
PartitionBoundInfos can't be "logically" equal unless their isnulls
pointers are the same, which is not the case unless they are
physically the same PartitionBoundInfo. What this means for its only
caller compute_partition_bounds() is that it now always needs to
perform partition_bounds_merge() for a pair of list-partitioned
relations, even if they have exactly the same bounds.

So, I'd suggest removing the block.

if (b1->isnulls)
{
if (b1->isnulls[i][j] != b2->isnulls[i][j])
return false;
if (b1->isnulls[i][j])
continue;
}

See how range partitioning infinite values are handled. Also, place
this before the comment block that was added for the "!datumIsEqual()"
case.

Fixed. I feel the 'continue' block is not required and hence removed it.

Actually, you should've kept the continue block as Amul suggested and
remove the "else" from the following:

/* < the long comment snipped >*/
else if (!datumIsEqual(b1->datums[i][j], b2->datums[i][j],
parttypbyval[j], parttyplen[j]))
return false;

because with this, list bounds will never be passed to datumIsEqual()
for comparison, even if both are non-NULL.

IOW, the block of code should look as follows, including the comments:

/*
* If the bound datums can be NULL, check that the datums on
* both sides are either both NULL or not NULL.
*/
if (b1->isnulls)
{
if (b1->isnulls[i][j] != b2->isnulls[i][j])
return false;

/* Must not pass NULL datums to datumIsEqual(). */
if (b1->isnulls[i][j])
continue;
}

/* < the long comment snipped >*/
if (!datumIsEqual(b1->datums[i][j], b2->datums[i][j],
parttypbyval[j], parttyplen[j]))
return false;

Also, please remove the declarations of b1_isnull and b2_isnull to get
rid of the warnings.

--
Amit Langote
EDB: http://www.enterprisedb.com

#39Amit Langote
amitlangote09@gmail.com
In reply to: Amit Langote (#38)
Re: Multi-Column List Partitioning

On Thu, Dec 9, 2021 at 2:54 PM Amit Langote <amitlangote09@gmail.com> wrote:

Hi Nitin,

Was looking at warnings generated by v8:

partbounds.c:971:17: warning: unused variable 'b1_isnull' [-Wunused-variable]
bool b1_isnull = false;
^
partbounds.c:972:17: warning: unused variable 'b2_isnull' [-Wunused-variable]
bool b2_isnull = false;

And it seems they've resulted from the above change:

I meant: "below change:".

--
Amit Langote
EDB: http://www.enterprisedb.com

#40Amul Sul
sulamul@gmail.com
In reply to: Amit Langote (#38)
Re: Multi-Column List Partitioning

On Thu, Dec 9, 2021 at 11:24 AM Amit Langote <amitlangote09@gmail.com> wrote:

[....]

On Mon, Dec 6, 2021 at 10:57 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

Looks difficult to understand at first glance, how about the following:

if (b1->isnulls != b2->isnulls)
return false;

I don't think having this block is correct, because this says that two
PartitionBoundInfos can't be "logically" equal unless their isnulls
pointers are the same, which is not the case unless they are
physically the same PartitionBoundInfo. What this means for its only
caller compute_partition_bounds() is that it now always needs to
perform partition_bounds_merge() for a pair of list-partitioned
relations, even if they have exactly the same bounds.

So, I'd suggest removing the block.

Agreed, I too realized the same; the check is incorrect and have noted
it for the next post. But note that, we need a kind of check here otherwise,
how could two bounds be equal if one has nulls and the other doesn't.
Also, we would have a segmentation fault in the next block while
accessing b2->isnulls if that is null.

I would suggest check like this:

if ((b1->isnulls == NULL) != (b2->isnulls == NULL))
return false;

OR

if ((b1->isnulls) ^ (b2->isnulls))
return false;

Regards,
Amul

#41Amit Langote
amitlangote09@gmail.com
In reply to: Amul Sul (#40)
Re: Multi-Column List Partitioning

On Thu, Dec 9, 2021 at 3:12 PM Amul Sul <sulamul@gmail.com> wrote:

On Thu, Dec 9, 2021 at 11:24 AM Amit Langote <amitlangote09@gmail.com> wrote:

[....]

On Mon, Dec 6, 2021 at 10:57 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

Looks difficult to understand at first glance, how about the following:

if (b1->isnulls != b2->isnulls)
return false;

I don't think having this block is correct, because this says that two
PartitionBoundInfos can't be "logically" equal unless their isnulls
pointers are the same, which is not the case unless they are
physically the same PartitionBoundInfo. What this means for its only
caller compute_partition_bounds() is that it now always needs to
perform partition_bounds_merge() for a pair of list-partitioned
relations, even if they have exactly the same bounds.

So, I'd suggest removing the block.

Agreed, I too realized the same; the check is incorrect and have noted
it for the next post. But note that, we need a kind of check here otherwise,
how could two bounds be equal if one has nulls and the other doesn't.

We check partition strategy at the top and that ensures that isnulls
fields should either be both NULL or not, same as the block above that
checks 'kind'. Maybe adding an Assert inside the block makes sense,
like this:

/*
* If the bound datums can be NULL, check that the datums on
* both sides are either both NULL or not NULL.
*/
if (b1->isnulls != NULL)
{
/*
* Both bound collections have the same partition strategy,
* so the other side must allow NULL datums as well.
*/
Assert(b2->isnulls != NULL);

if (b1->isnulls[i][j] != b2->isnulls[i][j])
return false;

/* Must not pass NULL datums to datumIsEqual(). */
if (b1->isnulls[i][j])
continue;
}

/* < the long comment snipped >*/
if (!datumIsEqual(b1->datums[i][j], b2->datums[i][j],
parttypbyval[j], parttyplen[j]))
return false;

--
Amit Langote
EDB: http://www.enterprisedb.com

#42Amul Sul
sulamul@gmail.com
In reply to: Amit Langote (#41)
Re: Multi-Column List Partitioning

On Thu, Dec 9, 2021 at 12:03 PM Amit Langote <amitlangote09@gmail.com> wrote:

On Thu, Dec 9, 2021 at 3:12 PM Amul Sul <sulamul@gmail.com> wrote:

On Thu, Dec 9, 2021 at 11:24 AM Amit Langote <amitlangote09@gmail.com> wrote:

[....]

On Mon, Dec 6, 2021 at 10:57 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

Looks difficult to understand at first glance, how about the following:

if (b1->isnulls != b2->isnulls)
return false;

I don't think having this block is correct, because this says that two
PartitionBoundInfos can't be "logically" equal unless their isnulls
pointers are the same, which is not the case unless they are
physically the same PartitionBoundInfo. What this means for its only
caller compute_partition_bounds() is that it now always needs to
perform partition_bounds_merge() for a pair of list-partitioned
relations, even if they have exactly the same bounds.

So, I'd suggest removing the block.

Agreed, I too realized the same; the check is incorrect and have noted
it for the next post. But note that, we need a kind of check here otherwise,
how could two bounds be equal if one has nulls and the other doesn't.

We check partition strategy at the top and that ensures that isnulls
fields should either be both NULL or not, same as the block above that
checks 'kind'. Maybe adding an Assert inside the block makes sense,
like this:

/*
* If the bound datums can be NULL, check that the datums on
* both sides are either both NULL or not NULL.
*/
if (b1->isnulls != NULL)
{
/*
* Both bound collections have the same partition strategy,
* so the other side must allow NULL datums as well.
*/
Assert(b2->isnulls != NULL);

Make sense, thanks!

Regards,
Amul

#43Amul Sul
sulamul@gmail.com
In reply to: Amul Sul (#42)
Re: Multi-Column List Partitioning

On Thu, Dec 9, 2021 at 12:43 PM Amul Sul <sulamul@gmail.com> wrote:

On Thu, Dec 9, 2021 at 12:03 PM Amit Langote <amitlangote09@gmail.com> wrote:

On Thu, Dec 9, 2021 at 3:12 PM Amul Sul <sulamul@gmail.com> wrote:

On Thu, Dec 9, 2021 at 11:24 AM Amit Langote <amitlangote09@gmail.com> wrote:

[....]

On Mon, Dec 6, 2021 at 10:57 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

Looks difficult to understand at first glance, how about the following:

if (b1->isnulls != b2->isnulls)
return false;

I don't think having this block is correct, because this says that two
PartitionBoundInfos can't be "logically" equal unless their isnulls
pointers are the same, which is not the case unless they are
physically the same PartitionBoundInfo. What this means for its only
caller compute_partition_bounds() is that it now always needs to
perform partition_bounds_merge() for a pair of list-partitioned
relations, even if they have exactly the same bounds.

So, I'd suggest removing the block.

Agreed, I too realized the same; the check is incorrect and have noted
it for the next post. But note that, we need a kind of check here otherwise,
how could two bounds be equal if one has nulls and the other doesn't.

We check partition strategy at the top and that ensures that isnulls
fields should either be both NULL or not, same as the block above that
checks 'kind'. Maybe adding an Assert inside the block makes sense,
like this:

/*
* If the bound datums can be NULL, check that the datums on
* both sides are either both NULL or not NULL.
*/
if (b1->isnulls != NULL)
{
/*
* Both bound collections have the same partition strategy,
* so the other side must allow NULL datums as well.
*/
Assert(b2->isnulls != NULL);

Make sense, thanks!

In addition to Amit's suggestions, here are a few more:

+   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));
+

None of them really needed to be palloc0; also, as described
previously you can avoid the last three by using get_partition_col_*
directly.
---

+           i = 0;
+           foreach(cell2, rowexpr->args)
+           {

It's up to you, rather than using a separate index variable and
incrementing that at the end, I think we can use
foreach_current_index(cell2) which would look much nicer.
---

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

palloc0 is unnecessary for the "values".
---

        dest->datums[i] = &boundDatums[i * natts];
+       if (src->isnulls)
+           dest->isnulls[i] = (bool *) palloc(sizeof(bool) * natts);

I think you can allocate memory for isnulls the same way you do
allocate boundDatums and just do the memcpy.
---

+       for (i = 0; i < partnatts; i++)
+       {
+           if (outer_isnull && outer_isnull[i])
+           {
+               outer_has_null = true;
+               if (outer_map.merged_indexes[outer_index] == -1)
+                   consider_outer_null = true;
+           }

I am wondering why you are not breaking the loop once you set
consider_outer_null?
Note that if you do that then you need a separate loop for the
inner_isnull part.
---

@@ -1351,14 +1431,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);
+

I have doubts about the condition that allows reaching
merge_null_partitions() but I am not sure I am correct. I think if the
list values missing from the __inner side__ then we might need to
check only "inner_has_null" & "consider_inner_null" and merge the
same, but why is this code also checking "outer_has_null" &
"consider_outer_null". Correct me if I am missing something.
---

+           if (isnulls && isnulls[i])
+               cmpval = 0;     /* NULL "=" NULL */
+           else
+               cmpval = 1;     /* NULL ">" not-NULL */
+       }
+       else if (isnulls && isnulls[i])
+           cmpval = -1;        /* not-NULL "<" NULL */

I really doubt this assumption is correct; aren't those strict operators?
---

+get_list_partbound_value_string(List *bound_value)
+{
+   StringInfo      buf = makeStringInfo();
+   StringInfo      boundconstraint = makeStringInfo();

boundconstraint should be declared inside "if (ncols > 1)" block.
---

+   foreach(cell, bound_value)
+   {
+       Const      *val = castNode(Const, lfirst(cell));
+
+       appendStringInfoString(buf, sep);
+       get_const_expr(val, &context, -1);
+       sep = ", ";
+       ncols++;
+   }

I think no need to increment ncols every time, you have a list and you
can get that. Also, I think since you have ncols already, you can
prepend and append parenthesis before and after so that you can avoid
extra StringInfo.
---

 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;

Adding "partnatts" to this struct seems to be unnecessary, AFAIUC,
added that for partition_bound_accepts_nulls(), but we can easily get
that value from the partitioning key & pass an additional argument.
Also, no information about the length of the "isnulls" array.
---

I think it would be helpful if you could split the patch: one for
multi-value list partitioning and another for the partition wise join, thanks.

Regards,
Amul

#44Ashutosh Sharma
ashu.coek88@gmail.com
In reply to: Nitin Jadhav (#36)
Re: Multi-Column List Partitioning

Hi,

Is this okay?

postgres=# CREATE TABLE t1 (a int, b int) PARTITION BY LIST ( a, a, a );
CREATE TABLE

postgres=# CREATE TABLE t1_1 PARTITION OF t1 FOR VALUES IN ((1, 2, 3), (4,
5, 6));
CREATE TABLE

postgres=# \d t1
Partitioned table "public.t1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | |
b | integer | | |
Partition key: LIST (a, a, a)
Number of partitions: 1 (Use \d+ to list them.)

--

Also, getting some compiler warnings when building the source. please check.

--
With Regards,
Ashutosh Sharma.

On Mon, Dec 6, 2021 at 7:27 PM Nitin Jadhav <nitinjadhavpostgres@gmail.com>
wrote:

Show quoted text

Thank you for reviewing the patch.

partbounds.c: In function ‘get_qual_for_list.isra.18’:
partbounds.c:4284:29: warning: ‘boundinfo’ may be used uninitialized
in this function [-Wmaybe-uninitialized]
datumCopy(bound_info->datums[i][j],
~~~~~~~~~~^~~~~~~~
partbounds.c:4335:21: note: ‘boundinfo’ was declared here
PartitionBoundInfo boundinfo;
^~~~~~~~~
partbounds.c: In function ‘partition_bounds_merge’:
partbounds.c:1305:12: warning: ‘inner_isnull’ may be used
uninitialized in this function [-Wmaybe-uninitialized]
bool *inner_isnull;
^~~~~~~~~~~~
partbounds.c:1304:12: warning: ‘outer_isnull’ may be used
uninitialized in this function [-Wmaybe-uninitialized]
bool *outer_isnull;
^~~~~~~~~~~~

Fixed.

This function is unnecessarily complicated, I think you can avoid
inner for loops; simply replace for-loop-block with "if
(equal(lfirst(cell), new_bound)) return true".

Thank you for the suggestion. Fixed.

+ 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));
+
This allocation seems to be worthless, read ahead.

I think there is no need for this separate loop inside
transformPartitionListBounds, you can do that same in the next loop as
well. And instead of get_partition_col_* calling and storing, simply
use that directly as an argument to transformPartitionBoundValue().

Yes. The loop can be avoided and content of the above loop can be
included in the next loop but the next loop iterates over a list of
multi column datums. For each iteration, we need the information of
all the columns. The above data (colname, coltype, coltypmod and
partcollation) remains same for each iteration of the loop, If we
modify as suggested, then the function to fetch these information has
to be called every-time. To avoid this situation I have made a
separate loop outside which only runs as many number of columns and
stores in a variable which can be reused later. Please let me correct
if I am wrong.

I think this should be inside the "else" block after "!IsA(rowexpr,
RowExpr)" error and you can avoid IsA() check too.

This is required to handle the situation when one partition key is
mentioned and multiple values are provided in the partition bound
specification.

Looks difficult to understand at first glance, how about the following:

if (b1->isnulls != b2->isnulls)
return false;

if (b1->isnulls)
{
if (b1->isnulls[i][j] != b2->isnulls[i][j])
return false;
if (b1->isnulls[i][j])
continue;
}

See how range partitioning infinite values are handled. Also, place
this before the comment block that was added for the "!datumIsEqual()"
case.

Fixed. I feel the 'continue' block is not required and hence removed it.

Nothing wrong with this but if we could have checked "dest->isnulls"
instead of "src->isnulls" would be much better.

Here we are copying the data from 'src' to 'dest'. If there is no data
in 'src', it is unnecessary to copy. Hence checking 'src'.

Condition "key->strategy != PARTITION_STRATEGY_LIST" seems to be

unnecessary.

Fixed.

Can't be a single loop?

Yes. Fixed.

On Fri, Dec 3, 2021 at 7:26 PM Amul Sul <sulamul@gmail.com> wrote:

Hi,

Few comments for v7 patch, note that I haven't been through the
previous discussion, if any of the review comments that has been
already discussed & overridden, then please ignore here too:

partbounds.c: In function ‘get_qual_for_list.isra.18’:
partbounds.c:4284:29: warning: ‘boundinfo’ may be used uninitialized
in this function [-Wmaybe-uninitialized]
datumCopy(bound_info->datums[i][j],
~~~~~~~~~~^~~~~~~~
partbounds.c:4335:21: note: ‘boundinfo’ was declared here
PartitionBoundInfo boundinfo;
^~~~~~~~~
partbounds.c: In function ‘partition_bounds_merge’:
partbounds.c:1305:12: warning: ‘inner_isnull’ may be used
uninitialized in this function [-Wmaybe-uninitialized]
bool *inner_isnull;
^~~~~~~~~~~~
partbounds.c:1304:12: warning: ‘outer_isnull’ may be used
uninitialized in this function [-Wmaybe-uninitialized]
bool *outer_isnull;
^~~~~~~~~~~~

Got these warnings with gcc -O2 compilation.
----

/*
+ * 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;
+
+ Assert(list_length(elem) == list_length(new_bound));
+
+ 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;
+}

This function is unnecessarily complicated, I think you can avoid
inner for loops; simply replace for-loop-block with "if
(equal(lfirst(cell), new_bound)) return true".
----

+ 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));
+
This allocation seems to be worthless, read ahead.
----
+ 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);
+ }

I think there is no need for this separate loop inside
transformPartitionListBounds, you can do that same in the next loop as
well. And instead of get_partition_col_* calling and storing, simply
use that directly as an argument to transformPartitionBoundValue().
----

+
+ 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))));
+

I think this should be inside the "else" block after "!IsA(rowexpr,
RowExpr)" error and you can avoid IsA() check too.
----

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

Looks difficult to understand at first glance, how about the following:

if (b1->isnulls != b2->isnulls)
return false;

if (b1->isnulls)
{
if (b1->isnulls[i][j] != b2->isnulls[i][j])
return false;
if (b1->isnulls[i][j])
continue;
}

See how range partitioning infinite values are handled. Also, place
this before the comment block that was added for the "!datumIsEqual()"
case.
----

+       if (src->isnulls)
+           dest->isnulls[i] = (bool *) palloc(sizeof(bool) * natts);
...
+           if (src->isnulls)
+               dest->isnulls[i][j] = src->isnulls[i][j];
+
Nothing wrong with this but if we could have checked "dest->isnulls"
instead of "src->isnulls" would be much better.
----
-           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);
Condition "key->strategy != PARTITION_STRATEGY_LIST" seems to be

unnecessary.

----

+       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;
+           }
+       }

Can't be a single loop?
----

It would be helpful if you could run pgindent on your patch if not done

already.

----

That's all for now, I am yet to finish the complete patch reading and
understand the code flow, but I am out of time now.

Regards,
Amul

#45Amit Langote
amitlangote09@gmail.com
In reply to: Ashutosh Sharma (#44)
Re: Multi-Column List Partitioning

Hi,

On Mon, Dec 13, 2021 at 11:37 PM Ashutosh Sharma <ashu.coek88@gmail.com> wrote:

Hi,

Is this okay?

postgres=# CREATE TABLE t1 (a int, b int) PARTITION BY LIST ( a, a, a );
CREATE TABLE

postgres=# CREATE TABLE t1_1 PARTITION OF t1 FOR VALUES IN ((1, 2, 3), (4, 5, 6));
CREATE TABLE

postgres=# \d t1
Partitioned table "public.t1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | |
b | integer | | |
Partition key: LIST (a, a, a)
Number of partitions: 1 (Use \d+ to list them.)

I'd say it's not okay for a user to expect this to work sensibly, and
I don't think it would be worthwhile to write code to point that out
to the user if that is what you were implying.

--
Amit Langote
EDB: http://www.enterprisedb.com

#46Ashutosh Sharma
ashu.coek88@gmail.com
In reply to: Amit Langote (#45)
Re: Multi-Column List Partitioning

On Mon, Dec 20, 2021 at 7:04 PM Amit Langote <amitlangote09@gmail.com>
wrote:

Hi,

On Mon, Dec 13, 2021 at 11:37 PM Ashutosh Sharma <ashu.coek88@gmail.com>
wrote:

Hi,

Is this okay?

postgres=# CREATE TABLE t1 (a int, b int) PARTITION BY LIST ( a, a, a );
CREATE TABLE

postgres=# CREATE TABLE t1_1 PARTITION OF t1 FOR VALUES IN ((1, 2, 3),

(4, 5, 6));

CREATE TABLE

postgres=# \d t1
Partitioned table "public.t1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | |
b | integer | | |
Partition key: LIST (a, a, a)
Number of partitions: 1 (Use \d+ to list them.)

I'd say it's not okay for a user to expect this to work sensibly, and
I don't think it would be worthwhile to write code to point that out
to the user if that is what you were implying.

OK. As you wish.

--
With Regards,
Ashutosh Sharma.

#47Amit Langote
amitlangote09@gmail.com
In reply to: Ashutosh Sharma (#46)
Re: Multi-Column List Partitioning

On Tue, Dec 21, 2021 at 2:47 PM Ashutosh Sharma <ashu.coek88@gmail.com> wrote:

On Mon, Dec 20, 2021 at 7:04 PM Amit Langote <amitlangote09@gmail.com> wrote:

On Mon, Dec 13, 2021 at 11:37 PM Ashutosh Sharma <ashu.coek88@gmail.com> wrote:

Hi,

Is this okay?

postgres=# CREATE TABLE t1 (a int, b int) PARTITION BY LIST ( a, a, a );
CREATE TABLE

postgres=# CREATE TABLE t1_1 PARTITION OF t1 FOR VALUES IN ((1, 2, 3), (4, 5, 6));
CREATE TABLE

postgres=# \d t1
Partitioned table "public.t1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | |
b | integer | | |
Partition key: LIST (a, a, a)
Number of partitions: 1 (Use \d+ to list them.)

I'd say it's not okay for a user to expect this to work sensibly, and
I don't think it would be worthwhile to write code to point that out
to the user if that is what you were implying.

OK. As you wish.

Actually, we *do* have some code in check_new_partition_bound() to
point it out if an empty range is specified for a partition, something
that one (or a DDL script) may accidentally do:

/*
* First check if the resulting range would be empty with
* specified lower and upper bounds...
*/
cmpval = partition_rbound_cmp(key->partnatts,
key->partsupfunc,
key->partcollation,
lower->datums, lower->kind,
true, upper);
Assert(cmpval != 0);
if (cmpval > 0)
{
/* Point to problematic key in the lower datums list. */
PartitionRangeDatum *datum = list_nth(spec->lowerdatums,
cmpval - 1);

ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("empty range bound specified for
partition \"%s\"",
relname),
errdetail("Specified lower bound %s is
greater than or equal to upper bound %s.",

get_range_partbound_string(spec->lowerdatums),

get_range_partbound_string(spec->upperdatums)),
parser_errposition(pstate, datum->location)));
}

So one may wonder why we don't catch and point out more such user
mistakes, like the one in your example. It may not be hard to
implement a proof that the partition bound definition a user entered
results in a self-contradictory partition constraint using the
facilities given in predtest.c. (The empty-range proof seemed simple
enough to implement as the above block of code.) I don't however see
why we should do that for partition constraints if we don't do the
same for CHECK constraints; for example, the following definition,
while allowed, is not very useful:

create table foo (a int check (a = 1 and a = 2));
\d foo
Table "public.foo"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | |
Check constraints:
"foo_a_check" CHECK (a = 1 AND a = 2)

Maybe partitioning should be looked at differently than the free-form
CHECK constraints, but I'm not so sure. Or if others insist that it
may be worthwhile to improve the user experience in such cases, we
could do that as a separate patch than the patch to implement
multi-column list partitioning.

--
Amit Langote
EDB: http://www.enterprisedb.com

#48Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Amit Langote (#47)
2 attachment(s)
Re: Multi-Column List Partitioning

Thanks for reviewing.

Ok, colname can be fetched in advance but I don't think it worth it to
fetch coltype, coltypmod & partcollation; and, store in the
explicitly allocated memory, instead, you can directly call
get_partition_col_* inline functions.

Fixed.
---

/*
* If the bound datums can be NULL, check that the datums on
* both sides are either both NULL or not NULL.
*/
if (b1->isnulls)
{
if (b1->isnulls[i][j] != b2->isnulls[i][j])
return false;

/* Must not pass NULL datums to datumIsEqual(). */
if (b1->isnulls[i][j])
continue;
}

/* < the long comment snipped >*/
if (!datumIsEqual(b1->datums[i][j], b2->datums[i][j],
parttypbyval[j], parttyplen[j]))
return false;

Make sense. Fixed as per the suggestion.
---

+           i = 0;
+           foreach(cell2, rowexpr->args)
+           {

It's up to you, rather than using a separate index variable and
incrementing that at the end, I think we can use
foreach_current_index(cell2) which would look much nicer.

Thanks for the suggestion. I have removed the increment operation and
retained the index variable with a call to foreach_current_index()
since the index variable is required in 3 places. It looks better than
before.
---

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

palloc0 is unnecessary for the "values".

Fixed.
---

dest->datums[i] = &boundDatums[i * natts];
+       if (src->isnulls)
+           dest->isnulls[i] = (bool *) palloc(sizeof(bool) * natts);

I think you can allocate memory for isnulls the same way you do
allocate boundDatums and just do the memcpy.

Fixed.
---

+       for (i = 0; i < partnatts; i++)
+       {
+           if (outer_isnull && outer_isnull[i])
+           {
+               outer_has_null = true;
+               if (outer_map.merged_indexes[outer_index] == -1)
+                   consider_outer_null = true;
+           }

I am wondering why you are not breaking the loop once you set
consider_outer_null?
Note that if you do that then you need a separate loop for the
inner_isnull part.

Right. Fixed.
---

I have doubts about the condition that allows reaching
merge_null_partitions() but I am not sure I am correct. I think if the
list values missing from the __inner side__ then we might need to
check only "inner_has_null" & "consider_inner_null" and merge the
same, but why is this code also checking "outer_has_null" &
"consider_outer_null". Correct me if I am missing something.

You are correct. These conditions are not required. Fixed.
---

+           if (isnulls && isnulls[i])
+               cmpval = 0;     /* NULL "=" NULL */
+           else
+               cmpval = 1;     /* NULL ">" not-NULL */
+       }
+       else if (isnulls && isnulls[i])
+           cmpval = -1;        /* not-NULL "<" NULL */

I really doubt this assumption is correct; aren't those strict operators?

Now there are possibilities of multiple NULL values. We should have a
mechanism to sort it when the bound values contain Non NULL and NULL
values. As per the above logic we put the NULL values at the end.
Please let me know if I am wrong.
---

+get_list_partbound_value_string(List *bound_value)
+{
+   StringInfo      buf = makeStringInfo();
+   StringInfo      boundconstraint = makeStringInfo();

boundconstraint should be declared inside "if (ncols > 1)" block.

Fixed.
---

+   foreach(cell, bound_value)
+   {
+       Const      *val = castNode(Const, lfirst(cell));
+
+       appendStringInfoString(buf, sep);
+       get_const_expr(val, &context, -1);
+       sep = ", ";
+       ncols++;
+   }

I think no need to increment ncols every time, you have a list and you
can get that. Also, I think since you have ncols already, you can
prepend and append parenthesis before and after so that you can avoid
extra StringInfo.

Fixed.
---

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;

Adding "partnatts" to this struct seems to be unnecessary, AFAIUC,
added that for partition_bound_accepts_nulls(), but we can easily get
that value from the partitioning key & pass an additional argument.
Also, no information about the length of the "isnulls" array.

This is required during merge_list_bounds(). AFAIK partition key
information is not available here.

I think it would be helpful if you could split the patch: one for
multi-value list partitioning and another for the partition wise join, thanks.

I have split the patch into 2 patches. One is for the multi column
list partitioning core changes and the other is for partition-wise
join support. Each patch has its respective test cases in the
regression suit and regression tests run successfully on each patch.
Kindly let me know if any other changes are required here.

Thanks & Regards,
Nitin Jadhav

Show quoted text

On Tue, Dec 21, 2021 at 6:30 PM Amit Langote <amitlangote09@gmail.com> wrote:

On Tue, Dec 21, 2021 at 2:47 PM Ashutosh Sharma <ashu.coek88@gmail.com> wrote:

On Mon, Dec 20, 2021 at 7:04 PM Amit Langote <amitlangote09@gmail.com> wrote:

On Mon, Dec 13, 2021 at 11:37 PM Ashutosh Sharma <ashu.coek88@gmail.com> wrote:

Hi,

Is this okay?

postgres=# CREATE TABLE t1 (a int, b int) PARTITION BY LIST ( a, a, a );
CREATE TABLE

postgres=# CREATE TABLE t1_1 PARTITION OF t1 FOR VALUES IN ((1, 2, 3), (4, 5, 6));
CREATE TABLE

postgres=# \d t1
Partitioned table "public.t1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | |
b | integer | | |
Partition key: LIST (a, a, a)
Number of partitions: 1 (Use \d+ to list them.)

I'd say it's not okay for a user to expect this to work sensibly, and
I don't think it would be worthwhile to write code to point that out
to the user if that is what you were implying.

OK. As you wish.

Actually, we *do* have some code in check_new_partition_bound() to
point it out if an empty range is specified for a partition, something
that one (or a DDL script) may accidentally do:

/*
* First check if the resulting range would be empty with
* specified lower and upper bounds...
*/
cmpval = partition_rbound_cmp(key->partnatts,
key->partsupfunc,
key->partcollation,
lower->datums, lower->kind,
true, upper);
Assert(cmpval != 0);
if (cmpval > 0)
{
/* Point to problematic key in the lower datums list. */
PartitionRangeDatum *datum = list_nth(spec->lowerdatums,
cmpval - 1);

ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("empty range bound specified for
partition \"%s\"",
relname),
errdetail("Specified lower bound %s is
greater than or equal to upper bound %s.",

get_range_partbound_string(spec->lowerdatums),

get_range_partbound_string(spec->upperdatums)),
parser_errposition(pstate, datum->location)));
}

So one may wonder why we don't catch and point out more such user
mistakes, like the one in your example. It may not be hard to
implement a proof that the partition bound definition a user entered
results in a self-contradictory partition constraint using the
facilities given in predtest.c. (The empty-range proof seemed simple
enough to implement as the above block of code.) I don't however see
why we should do that for partition constraints if we don't do the
same for CHECK constraints; for example, the following definition,
while allowed, is not very useful:

create table foo (a int check (a = 1 and a = 2));
\d foo
Table "public.foo"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | |
Check constraints:
"foo_a_check" CHECK (a = 1 AND a = 2)

Maybe partitioning should be looked at differently than the free-form
CHECK constraints, but I'm not so sure. Or if others insist that it
may be worthwhile to improve the user experience in such cases, we
could do that as a separate patch than the patch to implement
multi-column list partitioning.

--
Amit Langote
EDB: http://www.enterprisedb.com

Attachments:

0001-multi-column-list-partitioning-core-changes.patchapplication/octet-stream; name=0001-multi-column-list-partitioning-core-changes.patchDownload
From 825725f00210e936fa2af51056aa369797231814 Mon Sep 17 00:00:00 2001
From: Nitin <nitin.jadhav@enterprisedb.com>
Date: Mon, 20 Dec 2021 18:46:28 +0530
Subject: [PATCH 1/2] multi column list partitioning

---
 src/backend/commands/tablecmds.c              |   7 -
 src/backend/executor/execPartition.c          |  10 +-
 src/backend/parser/parse_utilcmd.c            | 168 +++++--
 src/backend/partitioning/partbounds.c         | 639 +++++++++++++++++---------
 src/backend/partitioning/partprune.c          | 465 +++++++++++++------
 src/backend/utils/adt/ruleutils.c             |  44 +-
 src/include/partitioning/partbounds.h         |  16 +-
 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_prune.out | 432 +++++++++++++++++
 src/test/regress/sql/create_table.sql         |  35 +-
 src/test/regress/sql/insert.sql               |  64 +++
 src/test/regress/sql/partition_prune.sql      |  42 ++
 14 files changed, 1689 insertions(+), 434 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index bf42587..f124c29 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16863,13 +16863,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 2d857a3..fc9c5ee 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,26 @@ 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)
+	{
+		if (equal(lfirst(cell), new_bound))
+			return true;
+	}
+
+	return false;
+}
+
+/*
  * transformPartitionBound
  *
  * Transform a partition bound specification
@@ -3996,7 +4019,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 +4068,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 +4112,98 @@ 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 *));
+
+	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;
+		}
+	}
+
+	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],
+										 get_partition_col_typid(key, 0),
+										 get_partition_col_typmod(key, 0),
+										 get_partition_col_collation(key, 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))));
+
+			foreach(cell2, rowexpr->args)
+			{
+				int			idx = foreach_current_index(cell2);
+				Node	   *expr = lfirst(cell2);
+				Const	   *val =
+				transformPartitionBoundValue(pstate, expr, colname[i],
+											 get_partition_col_typid(key, idx),
+											 get_partition_col_typmod(key, idx),
+											 get_partition_col_collation(key, idx));
+
+				values = lappend(values, val);
+			}
+		}
+
+		/* Don't add to the result if the value is a duplicate */
+		if (!isListBoundDuplicated(result, values))
+			result = lappend(result, values);
+	}
+
+	pfree(colname);
+	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..dd75a25 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
@@ -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,10 @@ 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,27 +445,17 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 }
 
 /*
- * get_non_null_list_datum_count
- * 		Counts the number of non-null Datums in each partition.
+ * get_list_datum_count
+ * 		Returns the total number of datums in all the partitions.
  */
 static int
-get_non_null_list_datum_count(PartitionBoundSpec **boundspecs, int nparts)
+get_list_datum_count(PartitionBoundSpec **boundspecs, int nparts)
 {
 	int			i;
 	int			count = 0;
 
 	for (i = 0; i < nparts; i++)
-	{
-		ListCell   *lc;
-
-		foreach(lc, boundspecs[i]->listdatums)
-		{
-			Const	   *val = lfirst_node(Const, lc);
-
-			if (!val->constisnull)
-				count++;
-		}
-	}
+		count += list_length(boundspecs[i]->listdatums);
 
 	return count;
 }
@@ -472,7 +469,7 @@ 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;
@@ -480,17 +477,19 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	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 +513,42 @@ 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 *) palloc(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
+				{
+					null_index = i;
+					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 +559,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 +570,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)
@@ -628,7 +644,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 +661,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;
 			}
 		}
@@ -703,6 +708,8 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	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;
 
@@ -915,9 +922,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;
 
@@ -976,6 +980,27 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 				}
 
 				/*
+				 * If the bound datums can be NULL, check that the datums on
+				 * both sides are either both NULL or not NULL.
+				 */
+				if (b1->isnulls)
+				{
+					/*
+					 * Both bound collections have the same partition
+					 * strategy, so the other side must allow NULL datums as
+					 * well.
+					 */
+					Assert(b2->isnulls != NULL);
+
+					if (b1->isnulls[i][j] != b2->isnulls[i][j])
+						return false;
+
+					/* Must not pass NULL datums to datumIsEqual(). */
+					if (b1->isnulls[i][j])
+						continue;
+				}
+
+				/*
 				 * Compare the actual values. Note that it would be both
 				 * incorrect and unsafe to invoke the comparison operator
 				 * derived from the partitioning specification here.  It would
@@ -1018,6 +1043,7 @@ partition_bounds_copy(PartitionBoundInfo src,
 	bool		hash_part;
 	int			natts;
 	Datum	   *boundDatums;
+	bool	   *isnulls;
 
 	dest = (PartitionBoundInfo) palloc(sizeof(PartitionBoundInfoData));
 
@@ -1026,10 +1052,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)
 	{
@@ -1069,6 +1096,7 @@ partition_bounds_copy(PartitionBoundInfo src,
 	hash_part = (key->strategy == PARTITION_STRATEGY_HASH);
 	natts = hash_part ? 2 : partnatts;
 	boundDatums = palloc(ndatums * natts * sizeof(Datum));
+	isnulls = palloc(ndatums * natts * sizeof(bool));
 
 	for (i = 0; i < ndatums; i++)
 	{
@@ -1076,6 +1104,9 @@ partition_bounds_copy(PartitionBoundInfo src,
 
 		dest->datums[i] = &boundDatums[i * natts];
 
+		if (src->isnulls)
+			dest->isnulls[i] = &isnulls[i * natts];
+
 		for (j = 0; j < natts; j++)
 		{
 			bool		byval;
@@ -1092,10 +1123,16 @@ 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) &&
+				(src->isnulls == NULL || !src->isnulls[i][j]))
 				dest->datums[i][j] = datumCopy(src->datums[i][j],
 											   byval, typlen);
+
+			if (src->isnulls)
+				dest->isnulls[i] = memcpy(dest->isnulls[i], src->isnulls[i],
+										  sizeof(bool) * natts);
+
 		}
 	}
 
@@ -1229,6 +1266,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 +1304,15 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		int			cmpval;
 		Datum	   *merged_datum = NULL;
 		int			merged_index = -1;
+		bool	   *outer_isnull = NULL;
+		bool	   *inner_isnull = NULL;
+		bool	   *merged_isnull = NULL;
+
+		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)
 		{
@@ -1294,6 +1341,18 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 			}
 		}
 
+		if (outer_isnull && outer_isnull[0])
+		{
+			outer_pos++;
+			continue;
+		}
+
+		if (inner_isnull && inner_isnull[0])
+		{
+			inner_pos++;
+			continue;
+		}
+
 		/* Get the list values. */
 		outer_datums = outer_pos < outer_bi->ndatums ?
 			outer_bi->datums[outer_pos] : NULL;
@@ -1341,6 +1400,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				goto cleanup;
 
 			merged_datum = outer_datums;
+			merged_isnull = outer_isnull;
 
 			/* Move to the next pair of list values. */
 			outer_pos++;
@@ -1374,6 +1434,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				if (merged_index == -1)
 					goto cleanup;
 				merged_datum = outer_datums;
+				merged_isnull = outer_isnull;
 			}
 
 			/* Move to the next list value on the outer side. */
@@ -1408,6 +1469,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				if (merged_index == -1)
 					goto cleanup;
 				merged_datum = inner_datums;
+				merged_isnull = inner_isnull;
 			}
 
 			/* Move to the next list value on the inner side. */
@@ -1422,6 +1484,7 @@ 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);
 		}
 	}
 
@@ -1478,6 +1541,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 +1552,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 +1861,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,
@@ -2527,8 +2593,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,6 +2604,16 @@ build_merged_partition_bounds(char strategy, List *merged_datums,
 
 	merged_bounds = (PartitionBoundInfo) palloc(sizeof(PartitionBoundInfoData));
 	merged_bounds->strategy = strategy;
+
+	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);
@@ -2556,6 +2633,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,7 +2645,8 @@ 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;
@@ -3074,30 +3153,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 +3692,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 = -1;
+
+	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 +3743,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 +3757,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 +3930,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 +4034,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 +4064,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 +4084,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;
 			}
@@ -4070,6 +4201,107 @@ get_qual_for_hash(Relation parent, PartitionBoundSpec *spec)
 }
 
 /*
+ * get_qual_for_list_datums
+ *
+ * Returns an implicit-AND list of expressions to use as a list partition's
+ * constraint, given the partition bound structure.
+ */
+static List *
+get_qual_for_list_datums(PartitionKey key, PartitionBoundInfo bound_info,
+						 List *list_datums, Expr **key_col, bool is_default,
+						 bool *key_is_null, Expr **is_null_test)
+{
+	int			i;
+	int			j;
+	int			ndatums;
+	bool		is_null;
+	List	   *datum_elems = NIL;
+
+	if (is_default)
+		ndatums = bound_info->ndatums;
+	else
+		ndatums = list_length(list_datums);
+
+	for (i = 0; i < ndatums; i++)
+	{
+		List	   *and_args = NIL;
+		Expr	   *datum_elem = NULL;
+
+		/*
+		 * 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.
+		 */
+		for (j = 0; j < key->partnatts; j++)
+		{
+			Const	   *val = NULL;
+
+			if (is_default)
+				is_null = bound_info->isnulls[i][j];
+			else
+			{
+				List	   *listbound = list_nth(list_datums, i);
+
+				val = castNode(Const, list_nth(listbound, j));
+				is_null = val->constisnull;
+			}
+
+			if (is_null)
+			{
+				NullTest   *nulltest = makeNode(NullTest);
+
+				nulltest->arg = key_col[j];
+				nulltest->nulltesttype = IS_NULL;
+				nulltest->argisrow = false;
+				nulltest->location = -1;
+				key_is_null[j] = true;
+
+				if (key->partnatts > 1)
+					and_args = lappend(and_args, nulltest);
+				else
+					*is_null_test = (Expr *) nulltest;
+			}
+			else
+			{
+				if (is_default)
+				{
+					val = makeConst(key->parttypid[j],
+									key->parttypmod[j],
+									key->parttypcoll[j],
+									key->parttyplen[j],
+									datumCopy(bound_info->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,
+																key_col[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);
+	}
+
+	return datum_elems;
+}
+
+/*
  * get_qual_for_list
  *
  * Returns an implicit-AND list of expressions to use as a list partition's
@@ -4082,30 +4314,40 @@ static List *
 get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 {
 	PartitionKey key = RelationGetPartitionKey(parent);
-	List	   *result;
-	Expr	   *keyCol;
-	Expr	   *opexpr;
-	NullTest   *nulltest;
-	ListCell   *cell;
-	List	   *elems = NIL;
-	bool		list_has_null = false;
+	List	   *result = NIL;
+	Expr	   *datumtest;
+	Expr	   *is_null_test = NULL;
+	List	   *datum_elems = NIL;
+	bool		key_is_null[PARTITION_MAX_KEYS];
+	int			i,
+				j;
+	Expr	  **keyCol = (Expr **) palloc0(key->partnatts * sizeof(Expr *));
+	PartitionBoundInfo boundinfo = {0};
 
-	/*
-	 * 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
@@ -4114,119 +4356,84 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 	 */
 	if (spec->is_default)
 	{
-		int			i;
 		int			ndatums = 0;
 		PartitionDesc pdesc = RelationGetPartitionDesc(parent, false);
-		PartitionBoundInfo boundinfo = pdesc->boundinfo;
+
+		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;
-
-			/*
-			 * 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.
-			 */
-			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);
-		}
 	}
-	else
+
+	datum_elems = get_qual_for_list_datums(key, boundinfo, spec->listdatums,
+										   keyCol, spec->is_default, key_is_null,
+										   &is_null_test);
+
+	/*
+	 * 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++)
 	{
-		/*
-		 * Create list of Consts for the allowed values, excluding any nulls.
-		 */
-		foreach(cell, spec->listdatums)
+		if (!key_is_null[i])
 		{
-			Const	   *val = lfirst_node(Const, cell);
-
-			if (val->constisnull)
-				list_has_null = true;
-			else
-				elems = lappend(elems, copyObject(val));
+			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 (elems)
+	/*
+	 * 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)
 	{
-		/*
-		 * Generate the operator expression from the non-null partition
-		 * values.
-		 */
-		opexpr = make_partition_op_expr(key, 0, BTEqualStrategyNumber,
-										keyCol, (Expr *) elems);
+		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
-	{
-		/*
-		 * If there are no partition values, we don't need an operator
-		 * expression.
-		 */
-		opexpr = NULL;
-	}
-
-	if (!list_has_null)
-	{
-		/*
-		 * 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;
+		datumtest = NULL;
 
-		result = opexpr ? list_make2(nulltest, opexpr) : list_make1(nulltest);
-	}
-	else
+	/*
+	 * 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)
 	{
-		/*
-		 * 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;
+		Expr	   *orexpr = makeBoolExpr(OR_EXPR,
+										  list_make2(is_null_test, datumtest),
+										  -1);
 
-		if (opexpr)
-		{
-			Expr	   *or;
-
-			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..9c7d8f2 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,16 @@ 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,
@@ -2316,6 +2315,8 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 	{
 		NullTest   *nulltest = (NullTest *) clause;
 		Expr	   *arg = nulltest->arg;
+		Const	   *expr;
+		PartClauseInfo *partclause;
 
 		if (IsA(arg, RelabelType))
 			arg = ((RelabelType *) arg)->arg;
@@ -2324,9 +2325,32 @@ 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;
+		}
+
+		expr = makeConst(UNKNOWNOID, -1, InvalidOid, -2, (Datum) 0, true, false);
+		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 +2651,170 @@ get_matching_hash_bounds(PartitionPruneContext *context,
 											  boundinfo->nindexes - 1);
 	}
 
+	return result;
+}
+
+/*
+ * get_min_and_max_offset
+ *
+ * Fetches the minimum and maximum offset of the matching partitions.
+ */
+static void
+get_min_and_max_offset(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 +2823,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 +2838,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 +2849,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 +2864,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 +2873,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,24 +2886,40 @@ 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;
+
+				/*
+				 * 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_offset(context, partsupfunc, values, isnulls,
+									   nvalues, off, &minoff, &maxoff);
 
-			/* We have a match. Remove from the result. */
-			Assert(boundinfo->indexes[off] >= 0);
-			result->bound_offsets = bms_del_member(result->bound_offsets,
-												   off);
+				/* 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. */
 		result->scan_default = partition_bound_has_default(boundinfo);
-
 		return result;
 	}
 
@@ -2757,41 +2937,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_offset(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 +3001,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 +3030,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 +3082,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 +3537,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 +3559,17 @@ 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 +3587,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 +3658,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 +3707,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 +3733,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 +3756,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 +3766,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 8da525c..a3c9183 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2287,7 +2287,10 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				if (string)
 					appendStringInfo(&buf, " ON DELETE %s", string);
 
-				/* Add columns specified to SET NULL or SET DEFAULT if provided. */
+				/*
+				 * Add columns specified to SET NULL or SET DEFAULT if
+				 * provided.
+				 */
 				val = SysCacheGetAttr(CONSTROID, tup,
 									  Anum_pg_constraint_confdelsetcols, &isnull);
 				if (!isnull)
@@ -9461,10 +9464,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 = ", ";
 						}
 
@@ -12025,6 +12027,40 @@ 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();
+	deparse_context context;
+	ListCell   *cell;
+	char	   *sep = "";
+
+	memset(&context, 0, sizeof(deparse_context));
+	context.buf = buf;
+
+	if (list_length(bound_value) > 1)
+		appendStringInfoChar(buf, '(');
+
+	foreach(cell, bound_value)
+	{
+		Const	   *val = castNode(Const, lfirst(cell));
+
+		appendStringInfoString(buf, sep);
+		get_const_expr(val, &context, -1);
+		sep = ", ";
+	}
+
+	if (list_length(bound_value) > 1)
+		appendStringInfoChar(buf, ')');
+
+	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..2c9c97a 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 */
@@ -132,10 +135,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_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_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

0002-partition-wise-join-support.patchapplication/octet-stream; name=0002-partition-wise-join-support.patchDownload
From 75cb769d22a7ecfae4af813c931ad93025d1a7f9 Mon Sep 17 00:00:00 2001
From: Nitin <nitin.jadhav@enterprisedb.com>
Date: Tue, 21 Dec 2021 18:25:04 +0530
Subject: [PATCH 2/2] partition-wise join support

---
 src/backend/partitioning/partbounds.c        |  304 ++++---
 src/include/partitioning/partbounds.h        |    5 +-
 src/test/regress/expected/partition_join.out | 1257 ++++++++++++++++++++++++++
 src/test/regress/sql/partition_join.sql      |  257 ++++++
 4 files changed, 1680 insertions(+), 143 deletions(-)

diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index dd75a25..9b55e05 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -106,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,
@@ -147,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,
+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);
 static void merge_default_partitions(PartitionMap *outer_map,
 									 PartitionMap *inner_map,
 									 bool outer_has_default,
@@ -372,7 +372,6 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	boundinfo->strategy = key->strategy;
 	boundinfo->partnatts = key->partnatts;
 	/* No special hash partitions. */
-	boundinfo->null_index = -1;
 	boundinfo->isnulls = NULL;
 	boundinfo->default_index = -1;
 
@@ -445,6 +444,34 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 }
 
 /*
+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if any of the partition bounds contains a NULL value,
+ * FALSE otherwise.
+ */
+bool
+partition_bound_accepts_nulls(PartitionBoundInfo boundinfo)
+{
+	int			i;
+
+	if (!boundinfo->isnulls)
+		return false;
+
+	for (i = 0; i < boundinfo->ndatums; i++)
+	{
+		int			j;
+
+		for (j = 0; j < boundinfo->partnatts; j++)
+		{
+			if (boundinfo->isnulls[i][j])
+				return true;
+		}
+	}
+
+	return false;
+}
+
+/*
  * get_list_datum_count
  * 		Returns the total number of datums in all the partitions.
  */
@@ -475,7 +502,6 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	int			ndatums;
 	int			next_index = 0;
 	int			default_index = -1;
-	int			null_index = -1;
 	Datum	   *boundDatums;
 	bool	   *boundIsNulls;
 
@@ -484,7 +510,6 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	boundinfo->strategy = key->strategy;
 	boundinfo->partnatts = key->partnatts;
 	/* Will be set correctly below. */
-	boundinfo->null_index = -1;
 	boundinfo->default_index = -1;
 
 	ndatums = get_list_datum_count(boundspecs, nparts);
@@ -528,10 +553,7 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 				if (!val->constisnull)
 					all_values[j].values[k] = val->constvalue;
 				else
-				{
-					null_index = i;
 					all_values[j].isnulls[k] = true;
-				}
 
 				k++;
 			}
@@ -595,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)
 	{
@@ -706,8 +712,6 @@ 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. */
@@ -1139,7 +1143,6 @@ partition_bounds_copy(PartitionBoundInfo src,
 	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;
@@ -1199,7 +1202,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,
@@ -1243,7 +1247,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)
@@ -1255,8 +1260,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;
@@ -1307,6 +1310,11 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		bool	   *outer_isnull = NULL;
 		bool	   *inner_isnull = NULL;
 		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];
@@ -1341,24 +1349,38 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 			}
 		}
 
-		if (outer_isnull && outer_isnull[0])
-		{
-			outer_pos++;
-			continue;
-		}
-
-		if (inner_isnull && inner_isnull[0])
-		{
-			inner_pos++;
-			continue;
-		}
-
 		/* Get the list values. */
 		outer_datums = outer_pos < outer_bi->ndatums ?
 			outer_bi->datums[outer_pos] : NULL;
 		inner_datums = inner_pos < inner_bi->ndatums ?
 			inner_bi->datums[inner_pos] : NULL;
 
+		for (i = 0; i < partnatts; i++)
+		{
+			if (outer_isnull && outer_isnull[i])
+			{
+				outer_has_null = true;
+				if (outer_map.merged_indexes[outer_index] == -1)
+				{
+					consider_outer_null = true;
+					break;
+				}
+			}
+		}
+
+		for (i = 0; i < partnatts; i++)
+		{
+			if (inner_isnull && inner_isnull[i])
+			{
+				inner_has_null = true;
+				if (inner_map.merged_indexes[inner_index] == -1)
+				{
+					consider_inner_null = true;
+					break;
+				}
+			}
+		}
+
 		/*
 		 * We run this loop till both sides finish.  This allows us to avoid
 		 * duplicating code to handle the remaining values on the side which
@@ -1375,10 +1397,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)
@@ -1389,15 +1411,31 @@ 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;
@@ -1411,14 +1449,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 (inner_has_null)
+			{
+				if (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);
@@ -1433,10 +1487,11 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 													   &default_index);
 				if (merged_index == -1)
 					goto cleanup;
-				merged_datum = outer_datums;
-				merged_isnull = outer_isnull;
 			}
 
+			merged_datum = outer_datums;
+			merged_isnull = outer_isnull;
+
 			/* Move to the next list value on the outer side. */
 			outer_pos++;
 		}
@@ -1446,14 +1501,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);
@@ -1468,10 +1539,11 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 													   &default_index);
 				if (merged_index == -1)
 					goto cleanup;
-				merged_datum = inner_datums;
-				merged_isnull = inner_isnull;
 			}
 
+			merged_datum = inner_datums;
+			merged_isnull = inner_isnull;
+
 			/* Move to the next list value on the inner side. */
 			inner_pos++;
 		}
@@ -1488,26 +1560,6 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		}
 	}
 
-	/*
-	 * 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,
@@ -2220,48 +2272,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
@@ -2273,14 +2301,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,
-													 next_index);
+			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
@@ -2290,14 +2316,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,
-													 next_index);
+			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
@@ -2315,12 +2339,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,
-													outer_null, inner_null,
-													next_index);
-			Assert(*null_index >= 0);
+			merged_index = merge_matching_partitions(outer_map, inner_map,
+													 outer_null, inner_null,
+													 next_index);
 		}
 	}
+
+	return merged_index;
 }
 
 /*
@@ -2653,7 +2678,6 @@ build_merged_partition_bounds(char strategy, List *merged_datums,
 	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;
diff --git a/src/include/partitioning/partbounds.h b/src/include/partitioning/partbounds.h
index 2c9c97a..4afedce 100644
--- a/src/include/partitioning/partbounds.h
+++ b/src/include/partitioning/partbounds.h
@@ -92,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,
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/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);
-- 
1.8.3.1

#49Amul Sul
sulamul@gmail.com
In reply to: Nitin Jadhav (#48)
Re: Multi-Column List Partitioning

On Tue, Dec 21, 2021 at 6:34 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

---

+           if (isnulls && isnulls[i])
+               cmpval = 0;     /* NULL "=" NULL */
+           else
+               cmpval = 1;     /* NULL ">" not-NULL */
+       }
+       else if (isnulls && isnulls[i])
+           cmpval = -1;        /* not-NULL "<" NULL */

I really doubt this assumption is correct; aren't those strict operators?

Now there are possibilities of multiple NULL values. We should have a
mechanism to sort it when the bound values contain Non NULL and NULL
values. As per the above logic we put the NULL values at the end.
Please let me know if I am wrong.

Ok, but I am not sure about the comparison approach, let's see what
others think.

---

[...]

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;

Adding "partnatts" to this struct seems to be unnecessary, AFAIUC,
added that for partition_bound_accepts_nulls(), but we can easily get
that value from the partitioning key & pass an additional argument.
Also, no information about the length of the "isnulls" array.

This is required during merge_list_bounds(). AFAIK partition key
information is not available here.

You can get that as an argument, see merge_range_bounds().

I think it would be helpful if you could split the patch: one for
multi-value list partitioning and another for the partition wise join, thanks.

I have split the patch into 2 patches. One is for the multi column
list partitioning core changes and the other is for partition-wise
join support. Each patch has its respective test cases in the
regression suit and regression tests run successfully on each patch.
Kindly let me know if any other changes are required here.

Thanks, for the slit that is much helpful, I have a few comments for
the 0001 patch as follow:

+ char **colname = (char **) palloc0(partnatts * sizeof(char *));

palloc0 is unnecessary.
---

+ foreach(cell2, rowexpr->args)
+ {
+ int idx = foreach_current_index(cell2);
+ Node    *expr = lfirst(cell2);
+ Const    *val =
+ transformPartitionBoundValue(pstate, expr, colname[i],
+ get_partition_col_typid(key, idx),
+ get_partition_col_typmod(key, idx),
+ get_partition_col_collation(key, idx));
+
+ values = lappend(values, val);
+ }

Array index for colname should be "idx".
---

result->scan_default = partition_bound_has_default(boundinfo);
+
return result;
...

/* Always include the default partition if any. */
result->scan_default = partition_bound_has_default(boundinfo);
-
return result;

...
else
result->scan_default = partition_bound_has_default(boundinfo);
+
return result;
...

-               /* Add columns specified to SET NULL or SET DEFAULT if
provided. */
+               /*
+                * Add columns specified to SET NULL or SET DEFAULT if
+                * provided.
+                */

spurious change -- look like something not related to your patch.
--

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

I think this is not true for multi-value list partitions, we might
still want prune partitions for e.g. (100, IS NULL, 20). Correct me
if I am missing something here.
---

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

Similarly, why would this be true for list partitioning? How can we
prune partitions if values is for e.g. (100, <not given>, 20).
--

-       if (bms_is_member(keyno, opstep->nullkeys))
+       if (bms_is_member(keyno, opstep->nullkeys) &&
+           context->strategy != PARTITION_STRATEGY_LIST)
            continue;
Will that prune for all NULL partitioning key values?
---
+                           appendStringInfoString
+                               (buf,
get_list_partbound_value_string(lfirst(cell)));

Formatting is not quite right.
--

+/*
+ * get_min_and_max_offset
+ *
+ * Fetches the minimum and maximum offset of the matching partitions.
+ */

...

+/*
+ * get_min_or_max_off
+ *
+ * Fetches either minimum or maximum offset of the matching partitions
+ * depending on the value of is_min parameter.
+ */

I am not sure we really have to have separate functions but if needed
then I would prefer to have a separate function for each min and max
rather than combining.
---

+       if (part_scheme->strategy != PARTITION_STRATEGY_LIST)
+       {
+           *clause_is_not_null = (nulltest->nulltesttype == IS_NOT_NULL);
+           return PARTCLAUSE_MATCH_NULLNESS;
+       }
+
+       expr = makeConst(UNKNOWNOID, -1, InvalidOid, -2, (Datum) 0,
true, false);
+       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;

I still believe considering NULL value for match clause is not a
fundamentally correct thing. And that is only for List partitioning
which isn't aligned with the other partitioning.
---

Regards,
Amul

#50Nitin Jadhav
nitinjadhavpostgres@gmail.com
In reply to: Amul Sul (#49)
2 attachment(s)
Re: Multi-Column List Partitioning

Thanks for reviewing.

Adding "partnatts" to this struct seems to be unnecessary, AFAIUC,
added that for partition_bound_accepts_nulls(), but we can easily get
that value from the partitioning key & pass an additional argument.
Also, no information about the length of the "isnulls" array.

This is required during merge_list_bounds(). AFAIK partition key
information is not available here.

You can get that as an argument, see merge_range_bounds().

Fixed.
---

+ char **colname = (char **) palloc0(partnatts * sizeof(char *));

palloc0 is unnecessary.

Fixed.
---

+ foreach(cell2, rowexpr->args)
+ {
+ int idx = foreach_current_index(cell2);
+ Node    *expr = lfirst(cell2);
+ Const    *val =
+ transformPartitionBoundValue(pstate, expr, colname[i],
+ get_partition_col_typid(key, idx),
+ get_partition_col_typmod(key, idx),
+ get_partition_col_collation(key, idx));
+
+ values = lappend(values, val);
+ }

Array index for colname should be "idx".

Fixed.
---

result->scan_default = partition_bound_has_default(boundinfo);
+
return result;
...

/* Always include the default partition if any. */
result->scan_default = partition_bound_has_default(boundinfo);
-
return result;

...
else
result->scan_default = partition_bound_has_default(boundinfo);
+
return result;
...

-               /* Add columns specified to SET NULL or SET DEFAULT if
provided. */
+               /*
+                * Add columns specified to SET NULL or SET DEFAULT if
+                * provided.
+                */

spurious change -- look like something not related to your patch.

Fixed.
---

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

I think this is not true for multi-value list partitions, we might
still want prune partitions for e.g. (100, IS NULL, 20). Correct me
if I am missing something here.

AFAIK, the above condition/comments says that, either we should
include all keys or prefixes of the partition keys to get the
partition pruning results. For example if we have a table with 2
columns and both are present in the partition key. Let the column
names be 'a' and 'b'.

SELECT * FROM table WHERE a=1 AND b=1; - This query works for pruning
and it refers to a comment which says all partition keys are included.
SELECT * FROM table WHERE b=1; - Here partition pruning does not work
as it does not contain prefix of the partition keys.
SELECT * FROM table WHERE a=1; - This query works fine as column 'a'
is prefix of partition keys.

Please let me know if you need more information.
---

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

Similarly, why would this be true for list partitioning? How can we
prune partitions if values is for e.g. (100, <not given>, 20).

The above description holds good for this also. As per the current
design, partition pruning is not applicable for the above example.
Kindly confirm whether we should support such scenarios.
---

-       if (bms_is_member(keyno, opstep->nullkeys))
+       if (bms_is_member(keyno, opstep->nullkeys) &&
+           context->strategy != PARTITION_STRATEGY_LIST)
continue;
Will that prune for all NULL partitioning key values?

Yes. This allows pruning with NULL values for list partitioning.
---

+                           appendStringInfoString
+                               (buf,
get_list_partbound_value_string(lfirst(cell)));

Formatting is not quite right.

Fixed.
---

+/*
+ * get_min_and_max_offset
+ *
+ * Fetches the minimum and maximum offset of the matching partitions.
+ */

...

+/*
+ * get_min_or_max_off
+ *
+ * Fetches either minimum or maximum offset of the matching partitions
+ * depending on the value of is_min parameter.
+ */

I am not sure we really have to have separate functions but if needed
then I would prefer to have a separate function for each min and max
rather than combining.

If we don't make a separate function, then we have to include this
code in get_matching_list_bounds() which is already a big function. I
just made a separate function to not increase the complexity of
get_matching_list_bounds() and most of the code present in
get_min_or_max_off() is common for min and max calculation. If we make
it separate then there might be a lot of duplications. Please let me
know if you still feel if any action is required.
---

+       if (part_scheme->strategy != PARTITION_STRATEGY_LIST)
+       {
+           *clause_is_not_null = (nulltest->nulltesttype == IS_NOT_NULL);
+           return PARTCLAUSE_MATCH_NULLNESS;
+       }
+
+       expr = makeConst(UNKNOWNOID, -1, InvalidOid, -2, (Datum) 0,
true, false);
+       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;

I still believe considering NULL value for match clause is not a
fundamentally correct thing. And that is only for List partitioning
which isn't aligned with the other partitioning.

As other partitions which support multiple partition keys (Range
partitioning) do not support NULL values. This feature supports
multiple partition keys with list partitioning and it also supports
NULL values. With the existing design, I have tried to support this
feature with minimal changes as possible. If this is not the right
approach to support NULL values, I would like to know how we can
support multiple NULL values. Kindly provide more information.

Thanks & Regards,
Nitin Jadhav

Show quoted text

On Thu, Dec 23, 2021 at 6:33 PM Amul Sul <sulamul@gmail.com> wrote:

On Tue, Dec 21, 2021 at 6:34 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

---

+           if (isnulls && isnulls[i])
+               cmpval = 0;     /* NULL "=" NULL */
+           else
+               cmpval = 1;     /* NULL ">" not-NULL */
+       }
+       else if (isnulls && isnulls[i])
+           cmpval = -1;        /* not-NULL "<" NULL */

I really doubt this assumption is correct; aren't those strict operators?

Now there are possibilities of multiple NULL values. We should have a
mechanism to sort it when the bound values contain Non NULL and NULL
values. As per the above logic we put the NULL values at the end.
Please let me know if I am wrong.

Ok, but I am not sure about the comparison approach, let's see what
others think.

---

[...]

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;

Adding "partnatts" to this struct seems to be unnecessary, AFAIUC,
added that for partition_bound_accepts_nulls(), but we can easily get
that value from the partitioning key & pass an additional argument.
Also, no information about the length of the "isnulls" array.

This is required during merge_list_bounds(). AFAIK partition key
information is not available here.

You can get that as an argument, see merge_range_bounds().

I think it would be helpful if you could split the patch: one for
multi-value list partitioning and another for the partition wise join, thanks.

I have split the patch into 2 patches. One is for the multi column
list partitioning core changes and the other is for partition-wise
join support. Each patch has its respective test cases in the
regression suit and regression tests run successfully on each patch.
Kindly let me know if any other changes are required here.

Thanks, for the slit that is much helpful, I have a few comments for
the 0001 patch as follow:

+ char **colname = (char **) palloc0(partnatts * sizeof(char *));

palloc0 is unnecessary.
---

+ foreach(cell2, rowexpr->args)
+ {
+ int idx = foreach_current_index(cell2);
+ Node    *expr = lfirst(cell2);
+ Const    *val =
+ transformPartitionBoundValue(pstate, expr, colname[i],
+ get_partition_col_typid(key, idx),
+ get_partition_col_typmod(key, idx),
+ get_partition_col_collation(key, idx));
+
+ values = lappend(values, val);
+ }

Array index for colname should be "idx".
---

result->scan_default = partition_bound_has_default(boundinfo);
+
return result;
...

/* Always include the default partition if any. */
result->scan_default = partition_bound_has_default(boundinfo);
-
return result;

...
else
result->scan_default = partition_bound_has_default(boundinfo);
+
return result;
...

-               /* Add columns specified to SET NULL or SET DEFAULT if
provided. */
+               /*
+                * Add columns specified to SET NULL or SET DEFAULT if
+                * provided.
+                */

spurious change -- look like something not related to your patch.
--

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

I think this is not true for multi-value list partitions, we might
still want prune partitions for e.g. (100, IS NULL, 20). Correct me
if I am missing something here.
---

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

Similarly, why would this be true for list partitioning? How can we
prune partitions if values is for e.g. (100, <not given>, 20).
--

-       if (bms_is_member(keyno, opstep->nullkeys))
+       if (bms_is_member(keyno, opstep->nullkeys) &&
+           context->strategy != PARTITION_STRATEGY_LIST)
continue;
Will that prune for all NULL partitioning key values?
---
+                           appendStringInfoString
+                               (buf,
get_list_partbound_value_string(lfirst(cell)));

Formatting is not quite right.
--

+/*
+ * get_min_and_max_offset
+ *
+ * Fetches the minimum and maximum offset of the matching partitions.
+ */

...

+/*
+ * get_min_or_max_off
+ *
+ * Fetches either minimum or maximum offset of the matching partitions
+ * depending on the value of is_min parameter.
+ */

I am not sure we really have to have separate functions but if needed
then I would prefer to have a separate function for each min and max
rather than combining.
---

+       if (part_scheme->strategy != PARTITION_STRATEGY_LIST)
+       {
+           *clause_is_not_null = (nulltest->nulltesttype == IS_NOT_NULL);
+           return PARTCLAUSE_MATCH_NULLNESS;
+       }
+
+       expr = makeConst(UNKNOWNOID, -1, InvalidOid, -2, (Datum) 0,
true, false);
+       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;

I still believe considering NULL value for match clause is not a
fundamentally correct thing. And that is only for List partitioning
which isn't aligned with the other partitioning.
---

Regards,
Amul

Attachments:

v10-0001-multi-column-list-partitioning-core-changes.patchapplication/x-patch; name=v10-0001-multi-column-list-partitioning-core-changes.patchDownload
From 00f7fae900f2c5700521a8e75a6c5dfe8f07bf37 Mon Sep 17 00:00:00 2001
From: Nitin <nitin.jadhav@enterprisedb.com>
Date: Wed, 29 Dec 2021 11:29:13 +0530
Subject: [PATCH 1/2] multi column list partitioning core changes

---
 src/backend/commands/tablecmds.c              |   7 -
 src/backend/executor/execPartition.c          |  10 +-
 src/backend/parser/parse_utilcmd.c            | 168 ++++---
 src/backend/partitioning/partbounds.c         | 635 +++++++++++++++++---------
 src/backend/partitioning/partprune.c          | 462 +++++++++++++------
 src/backend/utils/adt/ruleutils.c             |  39 +-
 src/include/partitioning/partbounds.h         |  15 +-
 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_prune.out | 432 ++++++++++++++++++
 src/test/regress/sql/create_table.sql         |  35 +-
 src/test/regress/sql/insert.sql               |  64 +++
 src/test/regress/sql/partition_prune.sql      |  42 ++
 14 files changed, 1678 insertions(+), 432 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 45e59e3..0780cd8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16861,13 +16861,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 2d857a3..106e5a3 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,26 @@ 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)
+	{
+		if (equal(lfirst(cell), new_bound))
+			return true;
+	}
+
+	return false;
+}
+
+/*
  * transformPartitionBound
  *
  * Transform a partition bound specification
@@ -3996,7 +4019,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 +4068,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 +4112,98 @@ 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 **) palloc(partnatts * sizeof(char *));
+
+	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;
+		}
+	}
+
+	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],
+										 get_partition_col_typid(key, 0),
+										 get_partition_col_typmod(key, 0),
+										 get_partition_col_collation(key, 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))));
+
+			foreach(cell2, rowexpr->args)
+			{
+				int			idx = foreach_current_index(cell2);
+				Node	   *expr = lfirst(cell2);
+				Const	   *val =
+				transformPartitionBoundValue(pstate, expr, colname[idx],
+											 get_partition_col_typid(key, idx),
+											 get_partition_col_typmod(key, idx),
+											 get_partition_col_collation(key, idx));
+
+				values = lappend(values, val);
+			}
+		}
+
+		/* Don't add to the result if the value is a duplicate */
+		if (!isListBoundDuplicated(result, values))
+			result = lappend(result, values);
+	}
+
+	pfree(colname);
+	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..2ccee84 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
@@ -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,
@@ -367,6 +372,7 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	boundinfo->strategy = key->strategy;
 	/* No special hash partitions. */
 	boundinfo->null_index = -1;
+	boundinfo->isnulls = NULL;
 	boundinfo->default_index = -1;
 
 	hbounds = (PartitionHashBound *)
@@ -438,27 +444,17 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 }
 
 /*
- * get_non_null_list_datum_count
- * 		Counts the number of non-null Datums in each partition.
+ * get_list_datum_count
+ * 		Returns the total number of datums in all the partitions.
  */
 static int
-get_non_null_list_datum_count(PartitionBoundSpec **boundspecs, int nparts)
+get_list_datum_count(PartitionBoundSpec **boundspecs, int nparts)
 {
 	int			i;
 	int			count = 0;
 
 	for (i = 0; i < nparts; i++)
-	{
-		ListCell   *lc;
-
-		foreach(lc, boundspecs[i]->listdatums)
-		{
-			Const	   *val = lfirst_node(Const, lc);
-
-			if (!val->constisnull)
-				count++;
-		}
-	}
+		count += list_length(boundspecs[i]->listdatums);
 
 	return count;
 }
@@ -472,7 +468,7 @@ 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;
@@ -480,6 +476,7 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	int			default_index = -1;
 	int			null_index = -1;
 	Datum	   *boundDatums;
+	bool	   *boundIsNulls;
 
 	boundinfo = (PartitionBoundInfoData *)
 		palloc0(sizeof(PartitionBoundInfoData));
@@ -488,9 +485,9 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	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 +511,42 @@ 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 *) palloc(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
+				{
+					null_index = i;
+					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 +557,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 +568,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)
@@ -628,7 +642,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 +659,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;
 			}
 		}
@@ -703,6 +706,7 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	boundinfo->strategy = key->strategy;
 	/* There is no special null-accepting range partition. */
 	boundinfo->null_index = -1;
+	boundinfo->isnulls = NULL;
 	/* Will be set correctly below. */
 	boundinfo->default_index = -1;
 
@@ -915,9 +919,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;
 
@@ -976,6 +977,27 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 				}
 
 				/*
+				 * If the bound datums can be NULL, check that the datums on
+				 * both sides are either both NULL or not NULL.
+				 */
+				if (b1->isnulls)
+				{
+					/*
+					 * Both bound collections have the same partition
+					 * strategy, so the other side must allow NULL datums as
+					 * well.
+					 */
+					Assert(b2->isnulls != NULL);
+
+					if (b1->isnulls[i][j] != b2->isnulls[i][j])
+						return false;
+
+					/* Must not pass NULL datums to datumIsEqual(). */
+					if (b1->isnulls[i][j])
+						continue;
+				}
+
+				/*
 				 * Compare the actual values. Note that it would be both
 				 * incorrect and unsafe to invoke the comparison operator
 				 * derived from the partitioning specification here.  It would
@@ -1018,6 +1040,7 @@ partition_bounds_copy(PartitionBoundInfo src,
 	bool		hash_part;
 	int			natts;
 	Datum	   *boundDatums;
+	bool	   *isnulls;
 
 	dest = (PartitionBoundInfo) palloc(sizeof(PartitionBoundInfoData));
 
@@ -1026,10 +1049,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)
 	{
@@ -1069,6 +1093,7 @@ partition_bounds_copy(PartitionBoundInfo src,
 	hash_part = (key->strategy == PARTITION_STRATEGY_HASH);
 	natts = hash_part ? 2 : partnatts;
 	boundDatums = palloc(ndatums * natts * sizeof(Datum));
+	isnulls = palloc(ndatums * natts * sizeof(bool));
 
 	for (i = 0; i < ndatums; i++)
 	{
@@ -1076,6 +1101,9 @@ partition_bounds_copy(PartitionBoundInfo src,
 
 		dest->datums[i] = &boundDatums[i * natts];
 
+		if (src->isnulls)
+			dest->isnulls[i] = &isnulls[i * natts];
+
 		for (j = 0; j < natts; j++)
 		{
 			bool		byval;
@@ -1092,10 +1120,15 @@ 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) &&
+				(src->isnulls == NULL || !src->isnulls[i][j]))
 				dest->datums[i][j] = datumCopy(src->datums[i][j],
 											   byval, typlen);
+
+			if (src->isnulls)
+				dest->isnulls[i] = memcpy(dest->isnulls[i], src->isnulls[i],
+										  sizeof(bool) * natts);
 		}
 	}
 
@@ -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,15 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		int			cmpval;
 		Datum	   *merged_datum = NULL;
 		int			merged_index = -1;
+		bool	   *outer_isnull = NULL;
+		bool	   *inner_isnull = NULL;
+		bool	   *merged_isnull = NULL;
+
+		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)
 		{
@@ -1294,6 +1337,18 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 			}
 		}
 
+		if (outer_isnull && outer_isnull[0])
+		{
+			outer_pos++;
+			continue;
+		}
+
+		if (inner_isnull && inner_isnull[0])
+		{
+			inner_pos++;
+			continue;
+		}
+
 		/* Get the list values. */
 		outer_datums = outer_pos < outer_bi->ndatums ?
 			outer_bi->datums[outer_pos] : NULL;
@@ -1341,6 +1396,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				goto cleanup;
 
 			merged_datum = outer_datums;
+			merged_isnull = outer_isnull;
 
 			/* Move to the next pair of list values. */
 			outer_pos++;
@@ -1374,6 +1430,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				if (merged_index == -1)
 					goto cleanup;
 				merged_datum = outer_datums;
+				merged_isnull = outer_isnull;
 			}
 
 			/* Move to the next list value on the outer side. */
@@ -1408,6 +1465,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				if (merged_index == -1)
 					goto cleanup;
 				merged_datum = inner_datums;
+				merged_isnull = inner_isnull;
 			}
 
 			/* Move to the next list value on the inner side. */
@@ -1422,6 +1480,7 @@ 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);
 		}
 	}
 
@@ -1478,6 +1537,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 +1548,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 +1857,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,
@@ -2527,8 +2589,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,6 +2600,16 @@ build_merged_partition_bounds(char strategy, List *merged_datums,
 
 	merged_bounds = (PartitionBoundInfo) palloc(sizeof(PartitionBoundInfoData));
 	merged_bounds->strategy = strategy;
+
+	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);
@@ -2556,6 +2629,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,7 +2641,8 @@ 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;
@@ -3074,30 +3149,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 +3688,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 = -1;
+
+	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 +3739,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 +3753,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 +3926,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 +4030,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 +4060,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 +4080,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;
 			}
@@ -4070,6 +4197,107 @@ get_qual_for_hash(Relation parent, PartitionBoundSpec *spec)
 }
 
 /*
+ * get_qual_for_list_datums
+ *
+ * Returns an implicit-AND list of expressions to use as a list partition's
+ * constraint, given the partition bound structure.
+ */
+static List *
+get_qual_for_list_datums(PartitionKey key, PartitionBoundInfo bound_info,
+						 List *list_datums, Expr **key_col, bool is_default,
+						 bool *key_is_null, Expr **is_null_test)
+{
+	int			i;
+	int			j;
+	int			ndatums;
+	bool		is_null;
+	List	   *datum_elems = NIL;
+
+	if (is_default)
+		ndatums = bound_info->ndatums;
+	else
+		ndatums = list_length(list_datums);
+
+	for (i = 0; i < ndatums; i++)
+	{
+		List	   *and_args = NIL;
+		Expr	   *datum_elem = NULL;
+
+		/*
+		 * 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.
+		 */
+		for (j = 0; j < key->partnatts; j++)
+		{
+			Const	   *val = NULL;
+
+			if (is_default)
+				is_null = bound_info->isnulls[i][j];
+			else
+			{
+				List	   *listbound = list_nth(list_datums, i);
+
+				val = castNode(Const, list_nth(listbound, j));
+				is_null = val->constisnull;
+			}
+
+			if (is_null)
+			{
+				NullTest   *nulltest = makeNode(NullTest);
+
+				nulltest->arg = key_col[j];
+				nulltest->nulltesttype = IS_NULL;
+				nulltest->argisrow = false;
+				nulltest->location = -1;
+				key_is_null[j] = true;
+
+				if (key->partnatts > 1)
+					and_args = lappend(and_args, nulltest);
+				else
+					*is_null_test = (Expr *) nulltest;
+			}
+			else
+			{
+				if (is_default)
+				{
+					val = makeConst(key->parttypid[j],
+									key->parttypmod[j],
+									key->parttypcoll[j],
+									key->parttyplen[j],
+									datumCopy(bound_info->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,
+																key_col[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);
+	}
+
+	return datum_elems;
+}
+
+/*
  * get_qual_for_list
  *
  * Returns an implicit-AND list of expressions to use as a list partition's
@@ -4082,30 +4310,40 @@ static List *
 get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 {
 	PartitionKey key = RelationGetPartitionKey(parent);
-	List	   *result;
-	Expr	   *keyCol;
-	Expr	   *opexpr;
-	NullTest   *nulltest;
-	ListCell   *cell;
-	List	   *elems = NIL;
-	bool		list_has_null = false;
+	List	   *result = NIL;
+	Expr	   *datumtest;
+	Expr	   *is_null_test = NULL;
+	List	   *datum_elems = NIL;
+	bool		key_is_null[PARTITION_MAX_KEYS];
+	int			i,
+				j;
+	Expr	  **keyCol = (Expr **) palloc0(key->partnatts * sizeof(Expr *));
+	PartitionBoundInfo boundinfo = {0};
 
-	/*
-	 * 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
@@ -4114,119 +4352,84 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 	 */
 	if (spec->is_default)
 	{
-		int			i;
 		int			ndatums = 0;
 		PartitionDesc pdesc = RelationGetPartitionDesc(parent, false);
-		PartitionBoundInfo boundinfo = pdesc->boundinfo;
+
+		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;
-
-			/*
-			 * 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.
-			 */
-			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);
-		}
 	}
-	else
+
+	datum_elems = get_qual_for_list_datums(key, boundinfo, spec->listdatums,
+										   keyCol, spec->is_default, key_is_null,
+										   &is_null_test);
+
+	/*
+	 * 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++)
 	{
-		/*
-		 * Create list of Consts for the allowed values, excluding any nulls.
-		 */
-		foreach(cell, spec->listdatums)
+		if (!key_is_null[i])
 		{
-			Const	   *val = lfirst_node(Const, cell);
-
-			if (val->constisnull)
-				list_has_null = true;
-			else
-				elems = lappend(elems, copyObject(val));
+			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 (elems)
+	/*
+	 * 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)
 	{
-		/*
-		 * Generate the operator expression from the non-null partition
-		 * values.
-		 */
-		opexpr = make_partition_op_expr(key, 0, BTEqualStrategyNumber,
-										keyCol, (Expr *) elems);
+		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
-	{
-		/*
-		 * If there are no partition values, we don't need an operator
-		 * expression.
-		 */
-		opexpr = NULL;
-	}
-
-	if (!list_has_null)
-	{
-		/*
-		 * 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;
+		datumtest = NULL;
 
-		result = opexpr ? list_make2(nulltest, opexpr) : list_make1(nulltest);
-	}
-	else
+	/*
+	 * 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)
 	{
-		/*
-		 * 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;
-
-		if (opexpr)
-		{
-			Expr	   *or;
+		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..c83c63a 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,16 @@ 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,
@@ -2316,6 +2315,8 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 	{
 		NullTest   *nulltest = (NullTest *) clause;
 		Expr	   *arg = nulltest->arg;
+		Const	   *expr;
+		PartClauseInfo *partclause;
 
 		if (IsA(arg, RelabelType))
 			arg = ((RelabelType *) arg)->arg;
@@ -2324,9 +2325,32 @@ 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;
+		}
 
-		return PARTCLAUSE_MATCH_NULLNESS;
+		expr = makeConst(UNKNOWNOID, -1, InvalidOid, -2, (Datum) 0, true, false);
+		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;
+		}
+
+		*pc = partclause;
+		return PARTCLAUSE_MATCH_CLAUSE;
 	}
 
 	/*
@@ -2627,13 +2651,170 @@ get_matching_hash_bounds(PartitionPruneContext *context,
 											  boundinfo->nindexes - 1);
 	}
 
+	return result;
+}
+
+/*
+ * get_min_and_max_offset
+ *
+ * Fetches the minimum and maximum offset of the matching partitions.
+ */
+static void
+get_min_and_max_offset(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 +2823,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 +2838,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 +2849,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 +2864,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,9 +2873,8 @@ 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 +2885,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;
+
+				/*
+				 * 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_offset(context, partsupfunc, values, isnulls,
+									   nvalues, off, &minoff, &maxoff);
 
-			/* We have a match. Remove from the result. */
-			Assert(boundinfo->indexes[off] >= 0);
-			result->bound_offsets = bms_del_member(result->bound_offsets,
-												   off);
+				/* 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,14 +2937,32 @@ 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_offset(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);
@@ -2774,24 +2972,17 @@ get_matching_list_bounds(PartitionPruneContext *context,
 			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 +3000,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 +3029,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 +3081,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 +3536,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 +3558,17 @@ 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 +3586,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 +3657,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 +3706,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 +3732,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 +3755,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 +3765,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 8da525c..a88ceda 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9461,10 +9461,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 = ", ";
 						}
 
@@ -12025,6 +12024,40 @@ 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();
+	deparse_context context;
+	ListCell   *cell;
+	char	   *sep = "";
+
+	memset(&context, 0, sizeof(deparse_context));
+	context.buf = buf;
+
+	if (list_length(bound_value) > 1)
+		appendStringInfoChar(buf, '(');
+
+	foreach(cell, bound_value)
+	{
+		Const	   *val = castNode(Const, lfirst(cell));
+
+		appendStringInfoString(buf, sep);
+		get_const_expr(val, &context, -1);
+		sep = ", ";
+	}
+
+	if (list_length(bound_value) > 1)
+		appendStringInfoChar(buf, ')');
+
+	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..7de5cb3 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
@@ -81,6 +82,7 @@ typedef struct PartitionBoundInfoData
 	char		strategy;		/* hash, list or range? */
 	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 */
@@ -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_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_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

v10-0002-partition-wise-join-support.patchapplication/x-patch; name=v10-0002-partition-wise-join-support.patchDownload
From 5dc2c3f34207a3608575c5625b209369f96832c3 Mon Sep 17 00:00:00 2001
From: Nitin <nitin.jadhav@enterprisedb.com>
Date: Wed, 29 Dec 2021 12:56:41 +0530
Subject: [PATCH 2/2] partition-wise join support

---
 src/backend/partitioning/partbounds.c        |  309 ++++---
 src/include/partitioning/partbounds.h        |    5 +-
 src/test/regress/expected/partition_join.out | 1257 ++++++++++++++++++++++++++
 src/test/regress/sql/partition_join.sql      |  257 ++++++
 4 files changed, 1683 insertions(+), 145 deletions(-)

diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index 2ccee84..258df06 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -106,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,
@@ -147,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,
+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);
 static void merge_default_partitions(PartitionMap *outer_map,
 									 PartitionMap *inner_map,
 									 bool outer_has_default,
@@ -371,7 +371,6 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
 	/* No special hash partitions. */
-	boundinfo->null_index = -1;
 	boundinfo->isnulls = NULL;
 	boundinfo->default_index = -1;
 
@@ -444,6 +443,34 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 }
 
 /*
+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if any of the partition bounds contains a NULL value,
+ * FALSE otherwise.
+ */
+bool
+partition_bound_accepts_nulls(PartitionBoundInfo boundinfo, int partnatts)
+{
+	int			i;
+
+	if (!boundinfo->isnulls)
+		return false;
+
+	for (i = 0; i < boundinfo->ndatums; i++)
+	{
+		int			j;
+
+		for (j = 0; j < partnatts; j++)
+		{
+			if (boundinfo->isnulls[i][j])
+				return true;
+		}
+	}
+
+	return false;
+}
+
+/*
  * get_list_datum_count
  * 		Returns the total number of datums in all the partitions.
  */
@@ -474,7 +501,6 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	int			ndatums;
 	int			next_index = 0;
 	int			default_index = -1;
-	int			null_index = -1;
 	Datum	   *boundDatums;
 	bool	   *boundIsNulls;
 
@@ -482,7 +508,6 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
 	/* Will be set correctly below. */
-	boundinfo->null_index = -1;
 	boundinfo->default_index = -1;
 
 	ndatums = get_list_datum_count(boundspecs, nparts);
@@ -526,10 +551,7 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 				if (!val->constisnull)
 					all_values[j].values[k] = val->constvalue;
 				else
-				{
-					null_index = i;
 					all_values[j].isnulls[k] = true;
-				}
 
 				k++;
 			}
@@ -593,22 +615,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)
 	{
@@ -704,8 +710,6 @@ 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->isnulls = NULL;
 	/* Will be set correctly below. */
 	boundinfo->default_index = -1;
@@ -1135,7 +1139,6 @@ partition_bounds_copy(PartitionBoundInfo src,
 	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;
@@ -1195,7 +1198,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,
@@ -1239,7 +1243,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)
@@ -1251,8 +1256,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;
@@ -1303,6 +1306,11 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		bool	   *outer_isnull = NULL;
 		bool	   *inner_isnull = NULL;
 		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];
@@ -1337,24 +1345,38 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 			}
 		}
 
-		if (outer_isnull && outer_isnull[0])
-		{
-			outer_pos++;
-			continue;
-		}
-
-		if (inner_isnull && inner_isnull[0])
-		{
-			inner_pos++;
-			continue;
-		}
-
 		/* Get the list values. */
 		outer_datums = outer_pos < outer_bi->ndatums ?
 			outer_bi->datums[outer_pos] : NULL;
 		inner_datums = inner_pos < inner_bi->ndatums ?
 			inner_bi->datums[inner_pos] : NULL;
 
+		for (i = 0; i < partnatts; i++)
+		{
+			if (outer_isnull && outer_isnull[i])
+			{
+				outer_has_null = true;
+				if (outer_map.merged_indexes[outer_index] == -1)
+				{
+					consider_outer_null = true;
+					break;
+				}
+			}
+		}
+
+		for (i = 0; i < partnatts; i++)
+		{
+			if (inner_isnull && inner_isnull[i])
+			{
+				inner_has_null = true;
+				if (inner_map.merged_indexes[inner_index] == -1)
+				{
+					consider_inner_null = true;
+					break;
+				}
+			}
+		}
+
 		/*
 		 * We run this loop till both sides finish.  This allows us to avoid
 		 * duplicating code to handle the remaining values on the side which
@@ -1371,10 +1393,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)
@@ -1385,15 +1407,31 @@ 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;
@@ -1407,14 +1445,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 (inner_has_null)
 			{
+				if (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);
@@ -1429,10 +1483,11 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 													   &default_index);
 				if (merged_index == -1)
 					goto cleanup;
-				merged_datum = outer_datums;
-				merged_isnull = outer_isnull;
 			}
 
+			merged_datum = outer_datums;
+			merged_isnull = outer_isnull;
+
 			/* Move to the next list value on the outer side. */
 			outer_pos++;
 		}
@@ -1442,14 +1497,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);
@@ -1464,10 +1535,11 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 													   &default_index);
 				if (merged_index == -1)
 					goto cleanup;
-				merged_datum = inner_datums;
-				merged_isnull = inner_isnull;
 			}
 
+			merged_datum = inner_datums;
+			merged_isnull = inner_isnull;
+
 			/* Move to the next list value on the inner side. */
 			inner_pos++;
 		}
@@ -1484,26 +1556,6 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		}
 	}
 
-	/*
-	 * 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,
@@ -2216,48 +2268,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
@@ -2269,14 +2297,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,
-													 next_index);
+			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
@@ -2286,14 +2312,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,
-													 next_index);
+			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
@@ -2311,12 +2335,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,
-													outer_null, inner_null,
-													next_index);
-			Assert(*null_index >= 0);
+			merged_index = merge_matching_partitions(outer_map, inner_map,
+													 outer_null, inner_null,
+													 next_index);
 		}
 	}
+
+	return merged_index;
 }
 
 /*
@@ -2649,7 +2674,6 @@ build_merged_partition_bounds(char strategy, List *merged_datums,
 	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;
@@ -3144,7 +3168,7 @@ check_new_partition_bound(char *relname, Relation parent,
 					Assert(boundinfo &&
 						   boundinfo->strategy == PARTITION_STRATEGY_LIST &&
 						   (boundinfo->ndatums > 0 ||
-							partition_bound_accepts_nulls(boundinfo) ||
+							partition_bound_accepts_nulls(boundinfo, key->partnatts) ||
 							partition_bound_has_default(boundinfo)));
 
 					foreach(cell, spec->listdatums)
@@ -4364,7 +4388,8 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 		 * If default is the only partition, there need not be any partition
 		 * constraint on it.
 		 */
-		if (ndatums == 0 && !partition_bound_accepts_nulls(boundinfo))
+		if (ndatums == 0 &&
+			!partition_bound_accepts_nulls(boundinfo, key->partnatts))
 			return NIL;
 
 	}
diff --git a/src/include/partitioning/partbounds.h b/src/include/partitioning/partbounds.h
index 7de5cb3..16af593 100644
--- a/src/include/partitioning/partbounds.h
+++ b/src/include/partitioning/partbounds.h
@@ -91,15 +91,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, int partnatts);
+
 extern int	get_hash_partition_greatest_modulus(PartitionBoundInfo b);
 extern uint64 compute_partition_hash_value(int partnatts, FmgrInfo *partsupfunc,
 										   Oid *partcollation,
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/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);
-- 
1.8.3.1

#51Amul Sul
sulamul@gmail.com
In reply to: Nitin Jadhav (#50)
Re: Multi-Column List Partitioning

On Wed, Dec 29, 2021 at 7:26 PM Nitin Jadhav
<nitinjadhavpostgres@gmail.com> wrote:

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

I think this is not true for multi-value list partitions, we might
still want prune partitions for e.g. (100, IS NULL, 20). Correct me
if I am missing something here.

AFAIK, the above condition/comments says that, either we should
include all keys or prefixes of the partition keys to get the
partition pruning results. For example if we have a table with 2
columns and both are present in the partition key. Let the column
names be 'a' and 'b'.

SELECT * FROM table WHERE a=1 AND b=1; - This query works for pruning
and it refers to a comment which says all partition keys are included.
SELECT * FROM table WHERE b=1; - Here partition pruning does not work
as it does not contain prefix of the partition keys.
SELECT * FROM table WHERE a=1; - This query works fine as column 'a'
is prefix of partition keys.

Please let me know if you need more information.

That what I was assuming is not correct. The dependency of the prefix
is true for the range partitioning but why should that be in the case
of list partitioning? I think all partitioning keys in the list will
not be dependent on each other, AFAICU. If you prune list partitions
based on the b=1 value that still is correct & gives the correct
result, correct me If I am wrong.

---

+/*
+ * get_min_and_max_offset
+ *
+ * Fetches the minimum and maximum offset of the matching partitions.
+ */

...

+/*
+ * get_min_or_max_off
+ *
+ * Fetches either minimum or maximum offset of the matching partitions
+ * depending on the value of is_min parameter.
+ */

I am not sure we really have to have separate functions but if needed
then I would prefer to have a separate function for each min and max
rather than combining.

If we don't make a separate function, then we have to include this
code in get_matching_list_bounds() which is already a big function. I
just made a separate function to not increase the complexity of
get_matching_list_bounds() and most of the code present in
get_min_or_max_off() is common for min and max calculation. If we make
it separate then there might be a lot of duplications. Please let me
know if you still feel if any action is required.

Hmm, ok, I personally didn't like to have two functions one gives max
and min and the other gives only max or min, the other could have
different opinions.

How about keeping only one function say, get_min_max_off() and based
on the argument e.g. minoff & maxoff fetch the value, I mean e.g. if
minoff is not null then fetch the value otherwise skip that, same for
maxoff too.

---

+       if (part_scheme->strategy != PARTITION_STRATEGY_LIST)
+       {
+           *clause_is_not_null = (nulltest->nulltesttype == IS_NOT_NULL);
+           return PARTCLAUSE_MATCH_NULLNESS;
+       }
+
+       expr = makeConst(UNKNOWNOID, -1, InvalidOid, -2, (Datum) 0,
true, false);
+       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;

I still believe considering NULL value for match clause is not a
fundamentally correct thing. And that is only for List partitioning
which isn't aligned with the other partitioning.

As other partitions which support multiple partition keys (Range
partitioning) do not support NULL values. This feature supports
multiple partition keys with list partitioning and it also supports
NULL values. With the existing design, I have tried to support this
feature with minimal changes as possible. If this is not the right
approach to support NULL values, I would like to know how we can
support multiple NULL values. Kindly provide more information.

I haven't studied the whole partition pruning code and don't know the
complete code flow, but AFAICU, this is not the correct way to handle null
value.

Regards,
Amul

#52Julien Rouhaud
rjuju123@gmail.com
In reply to: Nitin Jadhav (#50)
Re: Multi-Column List Partitioning

Hi,

The cfbot reports some clang warning on the last version of the patchset:

https://cirrus-ci.com/task/6721617647632384

[16:35:24.444] partprune.c:2775:8: error: explicitly assigning value of variable of type 'int' to itself [-Werror,-Wself-assign]
[16:35:24.444] off = off;
[16:35:24.444]

A quick look at the patch seems to indicate that it's harmless dead code that
should be removed, and there are no other failure reported by the cfbot, but I
definitely didn't read the patch carefully so a confirmation (and an updated
patch) would be welcome.

However I see that Amul raised some concerns, so I will change the patch status
to Waiting on Author. Feel free to switch it back to Needs Review if you think
it's more appropriate.