From 638e0d061a22ff5433a6170f6a68610f45187b08 Mon Sep 17 00:00:00 2001 From: Nitin 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