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"
