diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index ebccfea..ea26615 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -3889,6 +3889,17 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events, return all_fired; } +/* ---------- + * AfterTriggerQueueIsEmpty() + * + * True if there are no pending triggers in the queue. + * ---------- + */ +bool +AfterTriggerQueueIsEmpty(void) +{ + return (afterTriggers.query_depth == -1 && afterTriggers.events.head == NULL); +} /* ---------- * AfterTriggerBeginXact() diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 8c799d3..8d0c9be 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -884,6 +884,10 @@ InitPlan(QueryDesc *queryDesc, int eflags) i++; } + if (AfterTriggerQueueIsEmpty()) + ExecImplodePlan(&plan, estate); + + /* * Initialize the private state information for all the nodes in the query * tree. This opens files, allocates storage and leaves us ready to start diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c index d5e1273..06ac364 100644 --- a/src/backend/executor/execUtils.c +++ b/src/backend/executor/execUtils.c @@ -45,6 +45,7 @@ #include "access/relscan.h" #include "access/transam.h" #include "catalog/index.h" +#include "commands/trigger.h" #include "executor/execdebug.h" #include "nodes/nodeFuncs.h" #include "parser/parsetree.h" @@ -54,6 +55,11 @@ static bool get_last_attnums(Node *node, ProjectionInfo *projInfo); +static bool ExecCanSkipScanNode(Plan *scannode, EState *estate); +static Plan *ExecTryRemoveHashJoin(Plan *hashjoin, EState *estate); +static Plan *ExecTryRemoveNestedLoop(Plan *nestedloop, EState *estate); +static Plan *ExecTryRemoveMergeJoin(Plan *mergejoin, EState *estate); +static void ExecResetJoinVarnos(List *tlist); static bool index_recheck_constraint(Relation index, Oid *constr_procs, Datum *existing_values, bool *existing_isnull, Datum *new_values); @@ -661,6 +667,271 @@ get_last_attnums(Node *node, ProjectionInfo *projInfo) (void *) projInfo); } +/* Returns true if the scan node may be skipped, otherwise returns false */ +static bool +ExecCanSkipScanNode(Plan *scannode, EState *estate) +{ + Scan *scan = (Scan *) scannode; + RangeTblEntry *rte; + + switch (nodeTag(scannode)) + { + case T_IndexOnlyScan: + case T_IndexScan: + case T_SeqScan: + rte = (RangeTblEntry *) list_nth(estate->es_range_table, scan->scanrelid - 1); + return rte->skipJoinPossible; + default: + /* If it's not a scan node then we can't remove it */ + return false; + } +} + + +static Plan * +ExecTryRemoveHashJoin(Plan *hashjoin, EState *estate) +{ + bool canRemoveLeft = false; + bool canRemoveRight = false; + Plan *leftnode = hashjoin->lefttree; + Plan *rightnode = hashjoin->righttree; + + /* + * If the left node is NULL, then mode likely the node has already been + * removed, in this case we can skip it + */ + if (leftnode == NULL) + canRemoveLeft = true; + else + canRemoveLeft = ExecCanSkipScanNode(leftnode, estate); + + if (rightnode == NULL) + canRemoveRight = true; + else + { + if (nodeTag(rightnode) != T_Hash) + { + elog(ERROR, "HashJoin's righttree node should be a Hash node"); + return hashjoin; + } + + /* move to the node where the hash is getting tuples from */ + rightnode = rightnode->lefttree; + + /* + * If this node is NULL then most likely a hashjoin has been completely + * removed from below the T_Hash node. In this case we can certainly + * remove the right node as there's nothing under it. + */ + if (rightnode == NULL) + canRemoveRight = true; + else + canRemoveRight = ExecCanSkipScanNode(rightnode, estate); + } + + if (canRemoveLeft) + { + if (canRemoveRight) + return NULL; /* this join is not required at all */ + else + return rightnode; + } + else + { + if (canRemoveRight) + return leftnode; /* only left is required */ + else + return hashjoin; /* both sides are required */ + } +} + +static Plan * +ExecTryRemoveNestedLoop(Plan *nestedloop, EState *estate) +{ + bool canRemoveLeft = false; + bool canRemoveRight = false; + Plan *leftnode = nestedloop->lefttree; + Plan *rightnode = nestedloop->righttree; + + /* + * If the left node is NULL, then mode likely the node has already been + * removed, in this case we can skip it + */ + if (leftnode == NULL) + canRemoveLeft = true; + else + canRemoveLeft = ExecCanSkipScanNode(leftnode, estate); + + if (rightnode == NULL) + canRemoveRight = true; + else + canRemoveRight = ExecCanSkipScanNode(rightnode, estate); + + if (canRemoveLeft) + { + if (canRemoveRight) + return NULL; /* this join is not required at all */ + else + return rightnode; + } + else + { + if (canRemoveRight) + return leftnode; /* only left is required */ + else + return nestedloop; /* both sides are required */ + } +} + +static Plan * +ExecTryRemoveMergeJoin(Plan *mergejoin, EState *estate) +{ + bool canRemoveLeft = false; + bool canRemoveRight = false; + Plan *leftnode = mergejoin->lefttree; + Plan *rightnode = mergejoin->righttree; + + /* + * If the left node is NULL, then mode likely the node has already been + * removed, in this case we can skip it + */ + if (leftnode == NULL) + canRemoveLeft = true; + else + { + if (nodeTag(leftnode) == T_Sort) + { + /* move to the node where the merge join is getting tuples from */ + leftnode = leftnode->lefttree; + } + + canRemoveLeft = ExecCanSkipScanNode(leftnode, estate); + } + + if (rightnode == NULL) + canRemoveRight = true; + else + { + if (nodeTag(rightnode) == T_Sort) + { + /* move to the node where the hash is getting tuples from */ + rightnode = rightnode->lefttree; + } + + /* + * Check just in case the node from below the sort was already removed, + * if it has then there's no point in this side of the join. + */ + if (rightnode == NULL) + canRemoveRight = true; + else + canRemoveRight = ExecCanSkipScanNode(rightnode, estate); + } + + if (canRemoveLeft) + { + if (canRemoveRight) + return NULL; /* this join is not required at all */ + + /* + * Right is required, skip left but maintain any sort nodes above the + * scan node as sort order may be critical for the parent node. + * XXX: Is there any way which we can check if the Sort order is + * important to the parent? + */ + else + return mergejoin->righttree; + } + else + { + /* Left is required, but right is not, again keep the sort */ + if (canRemoveRight) + return mergejoin->lefttree; + else + return mergejoin; /* both sides are required */ + } +} + + +/* + * Reset a join node's targetlist Vars to remove the OUTER_VAR and INNER_VAR + * varnos + */ +static void +ExecResetJoinVarnos(List *tlist) +{ + ListCell *lc; + + foreach (lc, tlist) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Var *var = (Var *) tle->expr; + + if (IsA(var, Var)) + var->varno = var->varnoold; + } +} + +/* + * Recursively process the plan tree to "move-up" nodes that sit beneath join + * nodes of any joins which are deemed unnecessary by the planner during the + * join removal process. + */ +void +ExecImplodePlan(Plan **node, EState *estate) +{ + Plan *skippedToNode; + if (*node == NULL) + return; + + /* visit each node recursively */ + ExecImplodePlan(&(*node)->lefttree, estate); + ExecImplodePlan(&(*node)->righttree, estate); + + switch (nodeTag(*node)) + { + case T_HashJoin: + skippedToNode = ExecTryRemoveHashJoin(*node, estate); + break; + case T_NestLoop: + skippedToNode = ExecTryRemoveNestedLoop(*node, estate); + break; + case T_MergeJoin: + skippedToNode = ExecTryRemoveMergeJoin(*node, estate); + break; + default: + return; + } + + /* both sides of join were removed, so we've nothing more to do here. */ + if (skippedToNode == NULL) + { + *node = NULL; + return; + } + + /* + * If we've managed to move the node up a level, then we'd better also + * replace the targetlist of the new node with that of the original node. + * If we didn't do this then we might end up with columns in the result-set + * that the query did not ask for. + * + * Also, since the original node was a join type node, the targetlist will + * contain OUTER_VAR and INNER_VAR in place if the real varnos, so we must + * put these back to what they should be. + */ + if (skippedToNode != *node) + { + // FIXME: What else apart from Sort should not be changed? + if (nodeTag(skippedToNode) != T_Sort) + { + ExecResetJoinVarnos((*node)->targetlist); + skippedToNode->targetlist = (*node)->targetlist; + } + *node = skippedToNode; + } +} + /* ---------------- * ExecAssignProjectionInfo * diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 6b1bf7b..223bbc0 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -2025,6 +2025,7 @@ _copyRangeTblEntry(const RangeTblEntry *from) COPY_SCALAR_FIELD(lateral); COPY_SCALAR_FIELD(inh); COPY_SCALAR_FIELD(inFromCl); + COPY_SCALAR_FIELD(skipJoinPossible); COPY_SCALAR_FIELD(requiredPerms); COPY_SCALAR_FIELD(checkAsUser); COPY_BITMAPSET_FIELD(selectedCols); diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index d5db71d..8e1d984 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -2343,6 +2343,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b) COMPARE_SCALAR_FIELD(lateral); COMPARE_SCALAR_FIELD(inh); COMPARE_SCALAR_FIELD(inFromCl); + COMPARE_SCALAR_FIELD(skipJoinPossible); COMPARE_SCALAR_FIELD(requiredPerms); COMPARE_SCALAR_FIELD(checkAsUser); COMPARE_BITMAPSET_FIELD(selectedCols); diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index edbd09f..783e2e9 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -2453,6 +2453,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_BOOL_FIELD(lateral); WRITE_BOOL_FIELD(inh); WRITE_BOOL_FIELD(inFromCl); + WRITE_BOOL_FIELD(skipJoinPossible); WRITE_UINT_FIELD(requiredPerms); WRITE_OID_FIELD(checkAsUser); WRITE_BITMAPSET_FIELD(selectedCols); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index a3efdd4..b74c0aa 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -1250,6 +1250,7 @@ _readRangeTblEntry(void) READ_BOOL_FIELD(lateral); READ_BOOL_FIELD(inh); READ_BOOL_FIELD(inFromCl); + READ_BOOL_FIELD(skipJoinPossible); READ_UINT_FIELD(requiredPerms); READ_OID_FIELD(checkAsUser); READ_BITMAPSET_FIELD(selectedCols); diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c index 9919d27..33f8a90 100644 --- a/src/backend/optimizer/path/equivclass.c +++ b/src/backend/optimizer/path/equivclass.c @@ -49,8 +49,6 @@ static List *generate_join_implied_equalities_broken(PlannerInfo *root, Relids outer_relids, Relids nominal_inner_relids, RelOptInfo *inner_rel); -static Oid select_equality_operator(EquivalenceClass *ec, - Oid lefttype, Oid righttype); static RestrictInfo *create_join_clause(PlannerInfo *root, EquivalenceClass *ec, Oid opno, EquivalenceMember *leftem, @@ -1282,7 +1280,7 @@ generate_join_implied_equalities_broken(PlannerInfo *root, * * Returns InvalidOid if no operator can be found for this datatype combination */ -static Oid +Oid select_equality_operator(EquivalenceClass *ec, Oid lefttype, Oid righttype) { ListCell *lc; diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index e99d416..2fb76f4 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -32,13 +32,21 @@ #include "utils/lsyscache.h" /* local functions */ -static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo); +static bool innerjoin_is_removable(PlannerInfo *root, List *joinlist, + RangeTblRef *removalrtr, Relids ignoredrels); +static bool leftjoin_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo); +static bool relation_is_needed(PlannerInfo *root, Relids joinrelids, + RelOptInfo *rel, Relids ignoredrels); +static bool relation_has_foreign_key_for(PlannerInfo *root, RelOptInfo *rel, + RelOptInfo *referencedrel, List *referencing_vars, + List *index_vars, List *operator_list); +static bool expressions_match_foreign_key(ForeignKeyInfo *fk, List *fkvars, + List *indexvars, List *operators); static void remove_rel_from_query(PlannerInfo *root, int relid, Relids joinrelids); static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved); static Oid distinct_col_search(int colno, List *colnos, List *opids); - /* * remove_useless_joins * Check for relations that don't actually need to be joined at all, @@ -46,26 +54,94 @@ static Oid distinct_col_search(int colno, List *colnos, List *opids); * * We are passed the current joinlist and return the updated list. Other * data structures that have to be updated are accessible via "root". + * + * There are 2 methods here for removing joins. Joins such as LEFT JOINs + * which can be proved to be needless due to lack of use of any of the joining + * relation's columns and the existence of a unique index on a subset of the + * join clause, can simply be removed from the query plan at plan time. For + * certain other join types we make use of foreign keys to attempt to prove the + * join is needless, though, for these we're unable to be certain that the join + * is not required at plan time, as if the plan is executed when pending + * foreign key triggers have not yet been fired, then the foreign key is + * effectively violated until these triggers have fired. Removing a join in + * such a case could cause a query to produce incorrect results. + * + * Instead we handle this case by marking the RangeTblEntry for the relation + * with a special flag which tells the executor that it's possible that joining + * to this relation may not be required. The executor may then check this flag + * and choose to skip the join based on if there are foreign key triggers + * pending or not. */ List * remove_useless_joins(PlannerInfo *root, List *joinlist) { ListCell *lc; + Relids removedrels = NULL; /* - * We are only interested in relations that are left-joined to, so we can - * scan the join_info_list to find them easily. + * Start by analyzing INNER JOINed relations in order to determine if any + * of the relations can be ignored. */ restart: + foreach(lc, joinlist) + { + RangeTblRef *rtr = (RangeTblRef *) lfirst(lc); + RangeTblEntry *rte; + + if (!IsA(rtr, RangeTblRef)) + continue; + + rte = root->simple_rte_array[rtr->rtindex]; + + /* Don't try to remove this one again if we've already removed it */ + if (rte->skipJoinPossible == true) + continue; + + /* skip if the join can't be removed */ + if (!innerjoin_is_removable(root, joinlist, rtr, removedrels)) + continue; + + /* + * Since we're not actually removing the join here, we need to maintain + * a list of relations that we've "removed" so when we're checking if + * other relations can be removed we'll know that if the to be removed + * relation is only referenced by a relation that we've already removed + * that it can be safely assumed that the relation is not referenced by + * any useful relation. + */ + removedrels = bms_add_member(removedrels, rtr->rtindex); + + /* + * Make a mark for the executor to say that it may be able to skip + * joining to this relation. + */ + rte->skipJoinPossible = true; + + /* + * Restart the scan. This is necessary to ensure we find all removable + * joins independently of their ordering. (note that since we've added + * this relation to the removedrels, we may now realize that other + * relations can also be removed as they're only referenced by the one + * that we've just marked as possibly removable). + */ + goto restart; + } + + /* now process special joins. Currently only left joins are supported */ foreach(lc, root->join_info_list) { SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc); int innerrelid; int nremoved; - /* Skip if not removable */ - if (!join_is_removable(root, sjinfo)) - continue; + if (sjinfo->jointype == JOIN_LEFT) + { + /* Skip if not removable */ + if (!leftjoin_is_removable(root, sjinfo)) + continue; + } + else + continue; /* we don't support this join type */ /* * Currently, join_is_removable can only succeed when the sjinfo's @@ -91,12 +167,11 @@ restart: root->join_info_list = list_delete_ptr(root->join_info_list, sjinfo); /* - * Restart the scan. This is necessary to ensure we find all - * removable joins independently of ordering of the join_info_list - * (note that removal of attr_needed bits may make a join appear - * removable that did not before). Also, since we just deleted the - * current list cell, we'd have to have some kluge to continue the - * list scan anyway. + * Restart the scan. This is necessary to ensure we find all removable + * joins independently of their ordering. (note that removal of + * attr_needed bits may make a join, inner or outer, appear removable + * that did not before). Also, since we just deleted the current list + * cell, we'd have to have some kluge to continue the list scan anyway. */ goto restart; } @@ -136,8 +211,213 @@ clause_sides_match_join(RestrictInfo *rinfo, Relids outerrelids, } /* - * join_is_removable - * Check whether we need not perform this special join at all, because + * innerjoin_is_removable + * True if the join to removalrtr can be removed. + * + * In order to prove a relation which is inner joined is not required we must + * be sure that the join would emit exactly 1 row on the join condition. This + * differs from the logic which is used for proving LEFT JOINs can be removed, + * where it's possible to just check that a unique index exists on the relation + * being removed which has a set of columns that is a subset of the columns + * seen in the join condition. If no matching row is found then left join would + * not remove the non-matched row from the result set. This is not the case + * with INNER JOINs, so here we must use foreign keys as proof that the 1 row + * exists before we can allow any joins to be removed. + */ +static bool +innerjoin_is_removable(PlannerInfo *root, List *joinlist, + RangeTblRef *removalrtr, Relids ignoredrels) +{ + ListCell *lc; + RelOptInfo *removalrel; + + removalrel = find_base_rel(root, removalrtr->rtindex); + + /* + * As foreign keys may only reference base rels which have unique indexes, + * we needn't go any further if we're not dealing with a base rel, or if + * the base rel has no unique indexes. We'd also better abort if the + * rtekind is anything but a relation, as things like sub-queries may have + * grouping or distinct clauses that would cause us not to be able to use + * the foreign key to prove the existence of a row matching the join + * condition. We also abort if the rel has no eclass joins as such a rel + * could well be joined using some operator which is not an equality + * operator, or the rel may not even be inner joined at all. + * + * Here we actually only check if the rel has any indexes, ideally we'd be + * checking for unique indexes, but we could only determine that by looping + * over the indexlist, and this is likely too expensive a check to be worth + * it here. + */ + if (removalrel->reloptkind != RELOPT_BASEREL || + removalrel->rtekind != RTE_RELATION || + removalrel->has_eclass_joins == false || + removalrel->indexlist == NIL) + return false; + + /* + * Currently we disallow the removal if we find any baserestrictinfo items + * on the relation being removed. The reason for this is that these would + * filter out rows and make it so the foreign key cannot prove that we'll + * match exactly 1 row on the join condition. However, this check is + * currently probably a bit overly strict as it should be possible to just + * check and ensure that each Var seen in the baserestrictinfo is also + * present in an eclass and if so, just translate and move the whole + * baserestrictinfo over to the relation which has the foreign key to prove + * that this join is not needed. e.g: + * SELECT a.* FROM a INNER JOIN b ON a.b_id = b.id WHERE b.id = 1; + * could become: SELECT a.* FROM a WHERE a.b_id = 1; + */ + if (removalrel->baserestrictinfo != NIL) + return false; + + /* + * Currently only eclass joins are supported, so if there are any non + * eclass join quals then we'll report the join is non-removable. + */ + if (removalrel->joininfo != NIL) + return false; + + /* + * Now we'll search through each relation in the joinlist to see if we can + * find a relation which has a foreign key which references removalrel on + * the join condition. If we find a rel with a foreign key which matches + * the join condition exactly, then we can be sure that exactly 1 row will + * be matched on the join, if we also see that no Vars from the relation + * are needed, then we can report the join as removable. + */ + foreach (lc, joinlist) + { + RangeTblRef *rtr = (RangeTblRef *) lfirst(lc); + RelOptInfo *rel; + ListCell *lc2; + List *referencing_vars; + List *index_vars; + List *operator_list; + Relids joinrelids; + + /* we can't remove ourself, or anything other than RangeTblRefs */ + if (rtr == removalrtr || !IsA(rtr, RangeTblRef)) + continue; + + rel = find_base_rel(root, rtr->rtindex); + + /* + * The only relation type that can help us is a base rel with at least + * one foreign key defined, if there's no eclass joins then this rel + * is not going to help us prove the removalrel is not needed. + */ + if (rel->reloptkind != RELOPT_BASEREL || + rel->rtekind != RTE_RELATION || + rel->has_eclass_joins == false || + rel->fklist == NIL) + continue; + + /* + * Both rels have eclass joins, but do they have eclass joins to each + * other? Skip this rel if it does not. + */ + if (!have_relevant_eclass_joinclause(root, rel, removalrel)) + continue; + + joinrelids = bms_union(rel->relids, removalrel->relids); + + /* if any of the Vars from the relation are needed then abort */ + if (relation_is_needed(root, joinrelids, removalrel, ignoredrels)) + return false; + + referencing_vars = NIL; + index_vars = NIL; + operator_list = NIL; + + /* now populate the lists with the join condition Vars */ + foreach(lc2, root->eq_classes) + { + EquivalenceClass *ec = (EquivalenceClass *) lfirst(lc2); + + if (list_length(ec->ec_members) <= 1) + continue; + + if (bms_overlap(removalrel->relids, ec->ec_relids) && + bms_overlap(rel->relids, ec->ec_relids)) + { + ListCell *lc3; + Var *refvar = NULL; + Var *idxvar = NULL; + + /* + * Look at each member of the eclass and try to find a Var from + * each side of the join that we can append to the list of + * columns that should be checked against each foreign key. + * + * The following logic does not allow for join removals to take + * place for foreign keys that have duplicate columns on the + * referencing side of the foreign key, such as: + * (a,a) references (x,y) + * The use case for such a foreign key is likely small enough + * that we needn't bother making this code anymore complex to + * solve. If we find more than 1 Var from any of the rels then + * we'll bail out. + */ + foreach (lc3, ec->ec_members) + { + EquivalenceMember *ecm = (EquivalenceMember *) lfirst(lc3); + + Var *var = (Var *) ecm->em_expr; + + if (!IsA(var, Var)) + continue; /* Ignore Consts */ + + if (var->varno == rel->relid) + { + if (refvar != NULL) + return false; + refvar = var; + } + + else if (var->varno == removalrel->relid) + { + if (idxvar != NULL) + return false; + idxvar = var; + } + } + + if (refvar != NULL && idxvar != NULL) + { + Oid opno; + Oid reloid = root->simple_rte_array[refvar->varno]->relid; + + if (!get_attnotnull(reloid, refvar->varattno)) + return false; + + /* grab the correct equality operator for these two vars */ + opno = select_equality_operator(ec, refvar->vartype, idxvar->vartype); + + if (!OidIsValid(opno)) + return false; + + referencing_vars = lappend(referencing_vars, refvar); + index_vars = lappend(index_vars, idxvar); + operator_list = lappend_oid(operator_list, opno); + } + } + } + + if (referencing_vars != NULL) + { + if (relation_has_foreign_key_for(root, rel, removalrel, + referencing_vars, index_vars, operator_list)) + return true; /* removalrel can be removed */ + } + } + + return false; /* can't remove join */ +} + +/* + * leftjoin_is_removable + * Check whether we need not perform this left join at all, because * it will just duplicate its left input. * * This is true for a left join for which the join condition cannot match @@ -147,7 +427,7 @@ clause_sides_match_join(RestrictInfo *rinfo, Relids outerrelids, * above the join. */ static bool -join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo) +leftjoin_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo) { int innerrelid; RelOptInfo *innerrel; @@ -155,14 +435,14 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo) Relids joinrelids; List *clause_list = NIL; ListCell *l; - int attroff; + + Assert(sjinfo->jointype == JOIN_LEFT); /* - * Must be a non-delaying left join to a single baserel, else we aren't + * Must be a non-delaying join to a single baserel, else we aren't * going to be able to do anything with it. */ - if (sjinfo->jointype != JOIN_LEFT || - sjinfo->delay_upper_joins) + if (sjinfo->delay_upper_joins) return false; if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid)) @@ -206,52 +486,9 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo) /* Compute the relid set for the join we are considering */ joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand); - /* - * We can't remove the join if any inner-rel attributes are used above the - * join. - * - * Note that this test only detects use of inner-rel attributes in higher - * join conditions and the target list. There might be such attributes in - * pushed-down conditions at this join, too. We check that case below. - * - * As a micro-optimization, it seems better to start with max_attr and - * count down rather than starting with min_attr and counting up, on the - * theory that the system attributes are somewhat less likely to be wanted - * and should be tested last. - */ - for (attroff = innerrel->max_attr - innerrel->min_attr; - attroff >= 0; - attroff--) - { - if (!bms_is_subset(innerrel->attr_needed[attroff], joinrelids)) - return false; - } - - /* - * Similarly check that the inner rel isn't needed by any PlaceHolderVars - * that will be used above the join. We only need to fail if such a PHV - * actually references some inner-rel attributes; but the correct check - * for that is relatively expensive, so we first check against ph_eval_at, - * which must mention the inner rel if the PHV uses any inner-rel attrs as - * non-lateral references. Note that if the PHV's syntactic scope is just - * the inner rel, we can't drop the rel even if the PHV is variable-free. - */ - foreach(l, root->placeholder_list) - { - PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(l); - - if (bms_is_subset(phinfo->ph_needed, joinrelids)) - continue; /* PHV is not used above the join */ - if (bms_overlap(phinfo->ph_lateral, innerrel->relids)) - return false; /* it references innerrel laterally */ - if (!bms_overlap(phinfo->ph_eval_at, innerrel->relids)) - continue; /* it definitely doesn't reference innerrel */ - if (bms_is_subset(phinfo->ph_eval_at, innerrel->relids)) - return false; /* there isn't any other place to eval PHV */ - if (bms_overlap(pull_varnos((Node *) phinfo->ph_var->phexpr), - innerrel->relids)) - return false; /* it does reference innerrel */ - } + /* if the relation is referenced in the query then it cannot be removed */ + if (relation_is_needed(root, joinrelids, innerrel, NULL)) + return false; /* * Search for mergejoinable clauses that constrain the inner rel against @@ -368,6 +605,218 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo) return false; } +/* + * relation_is_needed + * True if any of the Vars from this relation are required in the query + */ +static inline bool +relation_is_needed(PlannerInfo *root, Relids joinrelids, RelOptInfo *rel, Relids ignoredrels) +{ + int attroff; + ListCell *l; + + /* + * rel is referenced if any of it's attributes are used above the join. + * + * Note that this test only detects use of rel's attributes in higher + * join conditions and the target list. There might be such attributes in + * pushed-down conditions at this join, too. We check that case below. + * + * As a micro-optimization, it seems better to start with max_attr and + * count down rather than starting with min_attr and counting up, on the + * theory that the system attributes are somewhat less likely to be wanted + * and should be tested last. + */ + for (attroff = rel->max_attr - rel->min_attr; + attroff >= 0; + attroff--) + { + if (!bms_is_subset(bms_difference(rel->attr_needed[attroff], ignoredrels), joinrelids)) + return true; + } + + /* + * Similarly check that rel isn't needed by any PlaceHolderVars that will + * be used above the join. We only need to fail if such a PHV actually + * references some of rel's attributes; but the correct check for that is + * relatively expensive, so we first check against ph_eval_at, which must + * mention rel if the PHV uses any of-rel's attrs as non-lateral + * references. Note that if the PHV's syntactic scope is just rel, we + * can't return true even if the PHV is variable-free. + */ + foreach(l, root->placeholder_list) + { + PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(l); + + if (bms_is_subset(phinfo->ph_needed, joinrelids)) + continue; /* PHV is not used above the join */ + if (bms_overlap(phinfo->ph_lateral, rel->relids)) + return true; /* it references rel laterally */ + if (!bms_overlap(phinfo->ph_eval_at, rel->relids)) + continue; /* it definitely doesn't reference rel */ + if (bms_is_subset(phinfo->ph_eval_at, rel->relids)) + return true; /* there isn't any other place to eval PHV */ + if (bms_overlap(pull_varnos((Node *) phinfo->ph_var->phexpr), + rel->relids)) + return true; /* it does reference rel */ + } + + return false; /* it does not reference rel */ +} + +/* + * relation_has_foreign_key_for + * Checks if rel has a foreign key which references referencedrel with the + * given list of expressions. + * + * For the match to succeed: + * referencing_vars must match the columns defined in the foreign key. + * index_vars must match the columns defined in the index for the foreign key. + */ +static bool +relation_has_foreign_key_for(PlannerInfo *root, RelOptInfo *rel, + RelOptInfo *referencedrel, List *referencing_vars, + List *index_vars, List *operator_list) +{ + ListCell *lc; + Oid refreloid; + + /* + * Look up the Oid of the referenced relation. We only want to look at + * foreign keys on the referencing relation which reference this relation. + */ + refreloid = root->simple_rte_array[referencedrel->relid]->relid; + + Assert(list_length(referencing_vars) > 0); + Assert(list_length(referencing_vars) == list_length(index_vars)); + Assert(list_length(referencing_vars) == list_length(operator_list)); + + /* + * Search through each foreign key on the referencing relation and try + * to find one which references the relation in the join condition. If we + * find one then we'll send the join conditions off to + * expressions_match_foreign_key() to see if they match the foreign key. + */ + foreach(lc, rel->fklist) + { + ForeignKeyInfo *fk = (ForeignKeyInfo *) lfirst(lc); + + if (fk->confrelid == refreloid) + { + if (expressions_match_foreign_key(fk, referencing_vars, + index_vars, operator_list)) + return true; + } + } + + return false; +} + +/* + * expressions_match_foreign_key + * True if the given fkvars, indexvars and operators will match + * exactly 1 record in the referenced relation of the foreign key. + * + * Note: This function expects fkvars and indexvars to only contain Var types. + * Expression indexes are not supported by foreign keys. + */ +static bool +expressions_match_foreign_key(ForeignKeyInfo *fk, List *fkvars, + List *indexvars, List *operators) +{ + ListCell *lc; + ListCell *lc2; + ListCell *lc3; + Bitmapset *allitems; + Bitmapset *matcheditems; + int lstidx; + int col; + + Assert(list_length(fkvars) == list_length(indexvars)); + Assert(list_length(fkvars) == list_length(operators)); + + /* + * Fast path out if there's not enough conditions to match each column in + * the foreign key. Note that we cannot check that the number of + * expressions are equal here since it would cause any expressions which + * are duplicated not to match. + */ + if (list_length(fkvars) < fk->conncols) + return false; + + /* + * We need to ensure that each foreign key column can be matched to a list + * item, and we need to ensure that each list item can be matched to a + * foreign key column. We do this by looping over each foreign key column + * and checking that we can find an item in the list which matches the + * current column, however this method does not allow us to ensure that no + * additional items exist in the list. We could solve that by performing + * another loop over each list item and check that it matches a foreign key + * column, but that's a bit wasteful. Instead we'll use 2 bitmapsets, one + * to store the 0 based index of each list item, and with the other we'll + * store each list index that we've managed to match. After we're done + * matching we'll just make sure that both bitmapsets are equal. + */ + allitems = NULL; + matcheditems = NULL; + + /* + * Build a bitmapset which contains each 1 based list index. It seems more + * efficient to do this in reverse so that we allocate enough memory for + * the bitmapset on first loop rather than reallocating each time we find + * we need a bit more space. + */ + for (lstidx = list_length(fkvars) - 1; lstidx >= 0; lstidx--) + allitems = bms_add_member(allitems, lstidx); + + for (col = 0; col < fk->conncols; col++) + { + bool matched = false; + + lstidx = 0; + + forthree(lc, fkvars, lc2, indexvars, lc3, operators) + { + Var *expr = (Var *) lfirst(lc); + Var *idxexpr = (Var *) lfirst(lc2); + Oid opr = lfirst_oid(lc3); + + Assert(IsA(expr, Var)); + Assert(IsA(idxexpr, Var)); + + /* Does this join qual match up to the current fkey column? */ + if (fk->conkey[col] == expr->varattno && + fk->confkey[col] == idxexpr->varattno && + equality_ops_are_compatible(opr, fk->conpfeqop[col])) + { + matched = true; + + /* mark this list item as matched */ + matcheditems = bms_add_member(matcheditems, lstidx); + + /* + * Don't break here as there may be duplicate expressions + * that we also need to match against. + */ + } + lstidx++; + } + + /* punt if there's no match. */ + if (!matched) + return false; + } + + /* + * Ensure that we managed to match every item in the list to a foreign key + * column. + */ + if (!bms_equal(allitems, matcheditems)) + return false; + + return true; /* matched */ +} + /* * Remove the target relid from the planner's data structures, having diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index f752ecc..a783f14 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -3712,6 +3712,7 @@ plan_cluster_use_sort(Oid tableOid, Oid indexOid) rte->lateral = false; rte->inh = false; rte->inFromCl = true; + rte->skipJoinPossible = false; query->rtable = list_make1(rte); /* Set up RTE/RelOptInfo arrays */ diff --git a/src/backend/optimizer/prep/prepsecurity.c b/src/backend/optimizer/prep/prepsecurity.c index b625b5c..74a0dca 100644 --- a/src/backend/optimizer/prep/prepsecurity.c +++ b/src/backend/optimizer/prep/prepsecurity.c @@ -311,6 +311,7 @@ expand_security_qual(PlannerInfo *root, List *tlist, int rt_index, subrte->security_barrier = rte->security_barrier; subrte->eref = copyObject(rte->eref); subrte->inFromCl = true; + subrte->skipJoinPossible = false; subquery->rtable = list_make1(subrte); subrtr = makeNode(RangeTblRef); diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index b2becfa..fea198e 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -25,7 +25,9 @@ #include "access/transam.h" #include "access/xlog.h" #include "catalog/catalog.h" +#include "catalog/pg_constraint.h" #include "catalog/heap.h" +#include "catalog/pg_type.h" #include "foreign/fdwapi.h" #include "miscadmin.h" #include "nodes/makefuncs.h" @@ -38,6 +40,7 @@ #include "parser/parsetree.h" #include "rewrite/rewriteManip.h" #include "storage/bufmgr.h" +#include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/snapmgr.h" @@ -89,6 +92,12 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, Relation relation; bool hasindex; List *indexinfos = NIL; + List *fkinfos = NIL; + Relation fkeyRel; + Relation fkeyRelIdx; + ScanKeyData fkeyScankey; + SysScanDesc fkeyScan; + HeapTuple tuple; /* * We need not lock the relation since it was already locked, either by @@ -384,6 +393,111 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, heap_close(relation, NoLock); + /* load foreign key constraints */ + ScanKeyInit(&fkeyScankey, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relationObjectId)); + + fkeyRel = heap_open(ConstraintRelationId, AccessShareLock); + fkeyRelIdx = index_open(ConstraintRelidIndexId, AccessShareLock); + fkeyScan = systable_beginscan_ordered(fkeyRel, fkeyRelIdx, NULL, 1, &fkeyScankey); + + while ((tuple = systable_getnext_ordered(fkeyScan, ForwardScanDirection)) != NULL) + { + Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tuple); + ForeignKeyInfo *fkinfo; + Datum adatum; + bool isNull; + ArrayType *arr; + int nelements; + + /* skip if not a foreign key */ + if (con->contype != CONSTRAINT_FOREIGN) + continue; + + /* we're not interested unless the fkey has been validated */ + if (!con->convalidated) + continue; + + fkinfo = (ForeignKeyInfo *) palloc(sizeof(ForeignKeyInfo)); + fkinfo->conindid = con->conindid; + fkinfo->confrelid = con->confrelid; + fkinfo->convalidated = con->convalidated; + fkinfo->conrelid = con->conrelid; + fkinfo->confupdtype = con->confupdtype; + fkinfo->confdeltype = con->confdeltype; + fkinfo->confmatchtype = con->confmatchtype; + + adatum = heap_getattr(tuple, Anum_pg_constraint_conkey, + RelationGetDescr(fkeyRel), &isNull); + + if (isNull) + elog(ERROR, "null conkey for constraint %u", + HeapTupleGetOid(tuple)); + + arr = DatumGetArrayTypeP(adatum); /* ensure not toasted */ + nelements = ARR_DIMS(arr)[0]; + if (ARR_NDIM(arr) != 1 || + nelements < 0 || + ARR_HASNULL(arr) || + ARR_ELEMTYPE(arr) != INT2OID) + elog(ERROR, "conkey is not a 1-D smallint array"); + + fkinfo->conkey = (int16 *) ARR_DATA_PTR(arr); + fkinfo->conncols = nelements; + + adatum = heap_getattr(tuple, Anum_pg_constraint_confkey, + RelationGetDescr(fkeyRel), &isNull); + + if (isNull) + elog(ERROR, "null confkey for constraint %u", + HeapTupleGetOid(tuple)); + + arr = DatumGetArrayTypeP(adatum); /* ensure not toasted */ + nelements = ARR_DIMS(arr)[0]; + + if (ARR_NDIM(arr) != 1 || + nelements < 0 || + ARR_HASNULL(arr) || + ARR_ELEMTYPE(arr) != INT2OID) + elog(ERROR, "confkey is not a 1-D smallint array"); + + /* sanity check */ + if (nelements != fkinfo->conncols) + elog(ERROR, "number of confkey elements does not equal conkey elements"); + + fkinfo->confkey = (int16 *) ARR_DATA_PTR(arr); + adatum = heap_getattr(tuple, Anum_pg_constraint_conpfeqop, + RelationGetDescr(fkeyRel), &isNull); + + if (isNull) + elog(ERROR, "null conpfeqop for constraint %u", + HeapTupleGetOid(tuple)); + + arr = DatumGetArrayTypeP(adatum); /* ensure not toasted */ + nelements = ARR_DIMS(arr)[0]; + + if (ARR_NDIM(arr) != 1 || + nelements < 0 || + ARR_HASNULL(arr) || + ARR_ELEMTYPE(arr) != OIDOID) + elog(ERROR, "conpfeqop is not a 1-D smallint array"); + + /* sanity check */ + if (nelements != fkinfo->conncols) + elog(ERROR, "number of conpfeqop elements does not equal conkey elements"); + + fkinfo->conpfeqop = (Oid *) ARR_DATA_PTR(arr); + + fkinfos = lappend(fkinfos, fkinfo); + } + + rel->fklist = fkinfos; + systable_endscan_ordered(fkeyScan); + index_close(fkeyRelIdx, AccessShareLock); + heap_close(fkeyRel, AccessShareLock); + /* * Allow a plugin to editorialize on the info we obtained from the * catalogs. Actions might include altering the assumed relation size, diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index 4c76f54..58d80bb 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -115,6 +115,7 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptKind reloptkind) rel->lateral_relids = NULL; rel->lateral_referencers = NULL; rel->indexlist = NIL; + rel->fklist = NIL; rel->pages = 0; rel->tuples = 0; rel->allvisfrac = 0; @@ -377,6 +378,7 @@ build_join_rel(PlannerInfo *root, joinrel->lateral_relids = NULL; joinrel->lateral_referencers = NULL; joinrel->indexlist = NIL; + joinrel->fklist = NIL; joinrel->pages = 0; joinrel->tuples = 0; joinrel->allvisfrac = 0; diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 478584d..cafeba9 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -1048,6 +1048,7 @@ addRangeTableEntry(ParseState *pstate, rte->lateral = false; rte->inh = inh; rte->inFromCl = inFromCl; + rte->skipJoinPossible = false; rte->requiredPerms = ACL_SELECT; rte->checkAsUser = InvalidOid; /* not set-uid by default, either */ @@ -1101,6 +1102,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->lateral = false; rte->inh = inh; rte->inFromCl = inFromCl; + rte->skipJoinPossible = false; rte->requiredPerms = ACL_SELECT; rte->checkAsUser = InvalidOid; /* not set-uid by default, either */ @@ -1179,6 +1181,7 @@ addRangeTableEntryForSubquery(ParseState *pstate, rte->lateral = lateral; rte->inh = false; /* never true for subqueries */ rte->inFromCl = inFromCl; + rte->skipJoinPossible = false; rte->requiredPerms = 0; rte->checkAsUser = InvalidOid; @@ -1433,6 +1436,7 @@ addRangeTableEntryForFunction(ParseState *pstate, rte->lateral = lateral; rte->inh = false; /* never true for functions */ rte->inFromCl = inFromCl; + rte->skipJoinPossible = false; rte->requiredPerms = 0; rte->checkAsUser = InvalidOid; @@ -1505,6 +1509,7 @@ addRangeTableEntryForValues(ParseState *pstate, rte->lateral = lateral; rte->inh = false; /* never true for values RTEs */ rte->inFromCl = inFromCl; + rte->skipJoinPossible = false; rte->requiredPerms = 0; rte->checkAsUser = InvalidOid; @@ -1573,6 +1578,7 @@ addRangeTableEntryForJoin(ParseState *pstate, rte->lateral = false; rte->inh = false; /* never true for joins */ rte->inFromCl = inFromCl; + rte->skipJoinPossible = false; rte->requiredPerms = 0; rte->checkAsUser = InvalidOid; @@ -1673,6 +1679,7 @@ addRangeTableEntryForCTE(ParseState *pstate, rte->lateral = false; rte->inh = false; /* never true for subqueries */ rte->inFromCl = inFromCl; + rte->skipJoinPossible = false; rte->requiredPerms = 0; rte->checkAsUser = InvalidOid; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 24ade6c..11ab914 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -843,6 +843,7 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty) oldrte->lateral = false; oldrte->inh = false; oldrte->inFromCl = true; + oldrte->skipJoinPossible = false; newrte = makeNode(RangeTblEntry); newrte->rtekind = RTE_RELATION; @@ -853,6 +854,7 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty) newrte->lateral = false; newrte->inh = false; newrte->inFromCl = true; + newrte->skipJoinPossible = false; /* Build two-element rtable */ memset(&dpns, 0, sizeof(dpns)); @@ -2508,6 +2510,7 @@ deparse_context_for(const char *aliasname, Oid relid) rte->lateral = false; rte->inh = false; rte->inFromCl = true; + rte->skipJoinPossible = false; /* Build one-element rtable */ dpns->rtable = list_make1(rte); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 73138e0..db0f90a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -916,6 +916,33 @@ get_atttypetypmodcoll(Oid relid, AttrNumber attnum, ReleaseSysCache(tp); } +/* + * get_attnotnull + * + * Given the relation id and the attribute number, + * return the "attnotnull" field from the attribute relation. + */ +bool +get_attnotnull(Oid relid, AttrNumber attnum) +{ + HeapTuple tp; + + tp = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(relid), + Int16GetDatum(attnum)); + if (HeapTupleIsValid(tp)) + { + Form_pg_attribute att_tup = (Form_pg_attribute) GETSTRUCT(tp); + bool result; + + result = att_tup->attnotnull; + ReleaseSysCache(tp); + return result; + } + else + return false; +} + /* ---------- COLLATION CACHE ---------- */ /* diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index d0b0356..34a75e4 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -181,6 +181,7 @@ extern void ExecBSTruncateTriggers(EState *estate, extern void ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo); +extern bool AfterTriggerQueueIsEmpty(void); extern void AfterTriggerBeginXact(void); extern void AfterTriggerBeginQuery(void); extern void AfterTriggerEndQuery(EState *estate); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index ed3ae39..1c2ef45 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -340,6 +340,7 @@ extern ProjectionInfo *ExecBuildProjectionInfo(List *targetList, ExprContext *econtext, TupleTableSlot *slot, TupleDesc inputDesc); +extern void ExecImplodePlan(Plan **planstate, EState *estate); extern void ExecAssignProjectionInfo(PlanState *planstate, TupleDesc inputDesc); extern void ExecFreeExprContext(PlanState *planstate); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 255415d..152f1bb 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -813,6 +813,8 @@ typedef struct RangeTblEntry bool lateral; /* subquery, function, or values is LATERAL? */ bool inh; /* inheritance requested? */ bool inFromCl; /* present in FROM clause? */ + bool skipJoinPossible; /* it may be possible to not bother joining + * this relation at all */ AclMode requiredPerms; /* bitmask of required access permissions */ Oid checkAsUser; /* if valid, check access as this role */ Bitmapset *selectedCols; /* columns needing SELECT permission */ diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h index 7116496..56918ab 100644 --- a/src/include/nodes/relation.h +++ b/src/include/nodes/relation.h @@ -359,6 +359,8 @@ typedef struct PlannerInfo * lateral_referencers - relids of rels that reference this one laterally * indexlist - list of IndexOptInfo nodes for relation's indexes * (always NIL if it's not a table) + * fklist - list of ForeignKeyInfo's for relation's foreign key + * constraints. (always NIL if it's not a table) * pages - number of disk pages in relation (zero if not a table) * tuples - number of tuples in relation (not considering restrictions) * allvisfrac - fraction of disk pages that are marked all-visible @@ -452,6 +454,7 @@ typedef struct RelOptInfo Relids lateral_relids; /* minimum parameterization of rel */ Relids lateral_referencers; /* rels that reference me laterally */ List *indexlist; /* list of IndexOptInfo */ + List *fklist; /* list of ForeignKeyInfo */ BlockNumber pages; /* size estimates derived from pg_class */ double tuples; double allvisfrac; @@ -542,6 +545,51 @@ typedef struct IndexOptInfo bool amhasgetbitmap; /* does AM have amgetbitmap interface? */ } IndexOptInfo; +/* + * ForeignKeyInfo + * Used to store pg_constraint records for foreign key constraints for use + * by the planner. + * + * conindid - The index which supports the foreign key + * + * confrelid - The relation that is referenced by this foreign key + * + * convalidated - True if the foreign key has been validated. + * + * conrelid - The Oid of the relation that the foreign key belongs to + * + * confupdtype - ON UPDATE action for when the referenced table is updated + * + * confdeltype - ON DELETE action, controls what to do when a record is + * deleted from the referenced table. + * + * confmatchtype - foreign key match type, e.g MATCH FULL, MATCH PARTIAL + * + * conncols - Number of columns defined in the foreign key + * + * conkey - An array of conncols elements to store the varattno of the + * columns on the referencing side of the foreign key + * + * confkey - An array of conncols elements to store the varattno of the + * columns on the referenced side of the foreign key + * + * conpfeqop - An array of conncols elements to store the operators for + * PK = FK comparisons + */ +typedef struct ForeignKeyInfo +{ + Oid conindid; /* index supporting this constraint */ + Oid confrelid; /* relation referenced by foreign key */ + bool convalidated; /* constraint has been validated? */ + Oid conrelid; /* relation this constraint constrains */ + char confupdtype; /* foreign key's ON UPDATE action */ + char confdeltype; /* foreign key's ON DELETE action */ + char confmatchtype; /* foreign key's match type */ + int conncols; /* number of columns references */ + int16 *conkey; /* Columns of conrelid that the constraint applies to */ + int16 *confkey; /* columns of confrelid that foreign key references */ + Oid *conpfeqop; /* Operator list for comparing PK to FK */ +} ForeignKeyInfo; /* * EquivalenceClasses diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index afa5f9b..6dada00 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -119,6 +119,8 @@ extern List *generate_join_implied_equalities(PlannerInfo *root, Relids join_relids, Relids outer_relids, RelOptInfo *inner_rel); +extern Oid select_equality_operator(EquivalenceClass *ec, Oid lefttype, + Oid righttype); extern bool exprs_known_equal(PlannerInfo *root, Node *item1, Node *item2); extern void add_child_rel_equivalences(PlannerInfo *root, AppendRelInfo *appinfo, diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 1a556f8..1cb9970 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -68,6 +68,7 @@ extern Oid get_atttype(Oid relid, AttrNumber attnum); extern int32 get_atttypmod(Oid relid, AttrNumber attnum); extern void get_atttypetypmodcoll(Oid relid, AttrNumber attnum, Oid *typid, int32 *typmod, Oid *collid); +extern bool get_attnotnull(Oid relid, AttrNumber attnum); extern char *get_collation_name(Oid colloid); extern char *get_constraint_name(Oid conoid); extern Oid get_opclass_family(Oid opclass); diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 2501184..7d44739 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -3276,6 +3276,197 @@ select i8.* from int8_tbl i8 left join (select f1 from int4_tbl group by f1) i4 (1 row) rollback; +begin work; +create temp table c ( + id int primary key +); +create temp table b ( + id int primary key, + c_id int not null, + val int not null, + constraint b_c_id_fkey foreign key (c_id) references c deferrable +); +create temp table a ( + id int primary key, + b_id int not null, + constraint a_b_id_fkey foreign key (b_id) references b deferrable +); +insert into c (id) values(1); +insert into b (id,c_id,val) values(2,1,10); +insert into a (id,b_id) values(3,2); +-- this should remove inner join to b +explain (costs off) +select a.* from a inner join b on a.b_id = b.id; + QUERY PLAN +--------------- + Seq Scan on a +(1 row) + +-- this should remove inner join to b and c +explain (costs off) +select a.* from a inner join b on a.b_id = b.id inner join c on b.c_id = c.id; + QUERY PLAN +--------------- + Seq Scan on a +(1 row) + +-- Ensure all of the target entries have their proper aliases. +select a.* from a inner join b on a.b_id = b.id inner join c on b.c_id = c.id; + id | b_id +----+------ + 3 | 2 +(1 row) + +-- change order of tables in query, this should generate the same plan as above. +explain (costs off) +select a.* from c inner join b on c.id = b.c_id inner join a on a.b_id = b.id; + QUERY PLAN +--------------- + Seq Scan on a +(1 row) + +-- inner join can't be removed due to b columns in the target list +explain (costs off) +select * from a inner join b on a.b_id = b.id; + QUERY PLAN +------------------------------ + Hash Join + Hash Cond: (a.b_id = b.id) + -> Seq Scan on a + -> Hash + -> Seq Scan on b +(5 rows) + +-- this should not remove inner join to b due to quals restricting results from b +explain (costs off) +select a.* from a inner join b on a.b_id = b.id where b.val = 10; + QUERY PLAN +---------------------------------- + Hash Join + Hash Cond: (a.b_id = b.id) + -> Seq Scan on a + -> Hash + -> Seq Scan on b + Filter: (val = 10) +(6 rows) + +-- check merge join nodes are removed properly +set enable_hashjoin = off; +-- this should remove joins to b and c. +explain (costs off) +select count(*) from a inner join b on a.b_id = b.id left join c on a.id = c.id; + QUERY PLAN +--------------------------- + Aggregate + -> Sort + Sort Key: a.b_id + -> Seq Scan on a +(4 rows) + +-- this should remove joins to b and c, however it b will only be removed on +-- 2nd attempt after c is removed by the left join removal code. +explain (costs off) +select count(*) from a inner join b on a.b_id = b.id left join c on b.id = c.id; + QUERY PLAN +--------------------------- + Aggregate + -> Sort + Sort Key: a.b_id + -> Seq Scan on a +(4 rows) + +set enable_hashjoin = on; +-- this should not remove join to b +explain (costs off) +select a.* from a inner join b on a.b_id = b.id where b.val = b.id; + QUERY PLAN +---------------------------------- + Hash Join + Hash Cond: (a.b_id = b.id) + -> Seq Scan on a + -> Hash + -> Seq Scan on b + Filter: (id = val) +(6 rows) + +-- this should not remove the join, no foreign key exists between a.id and b.id +explain (costs off) +select a.* from a inner join b on a.id = b.id; + QUERY PLAN +---------------------------- + Hash Join + Hash Cond: (a.id = b.id) + -> Seq Scan on a + -> Hash + -> Seq Scan on b +(5 rows) + +-- ensure a left joined rel can't remove an inner joined rel +explain (costs off) +select a.* from b left join a on b.id = a.b_id; + QUERY PLAN +------------------------------ + Hash Right Join + Hash Cond: (a.b_id = b.id) + -> Seq Scan on a + -> Hash + -> Seq Scan on b +(5 rows) + +-- Ensure we remove b, but don't try and remove c. c has no join condition. +explain (costs off) +select a.* from a inner join b on a.b_id = b.id cross join c; + QUERY PLAN +--------------------------- + Nested Loop + -> Seq Scan on c + -> Materialize + -> Seq Scan on a +(4 rows) + +set constraints b_c_id_fkey deferred; +-- join should be removed. +explain (costs off) +select b.* from b inner join c on b.c_id = c.id; + QUERY PLAN +--------------- + Seq Scan on b +(1 row) + +prepare ab as select b.* from b inner join c on b.c_id = c.id; +explain (costs off) +execute ab; + QUERY PLAN +--------------- + Seq Scan on b +(1 row) + +-- perform an update which will cause some pending fk triggers to be added +update c set id = 2 where id=1; +-- ensure inner join is no longer removed. +explain (costs off) +select b.* from b inner join c on b.c_id = c.id; + QUERY PLAN +------------------------------ + Hash Join + Hash Cond: (b.c_id = c.id) + -> Seq Scan on b + -> Hash + -> Seq Scan on c +(5 rows) + +explain (costs off) +execute ab; + QUERY PLAN +------------------------------ + Hash Join + Hash Cond: (b.c_id = c.id) + -> Seq Scan on b + -> Hash + -> Seq Scan on c +(5 rows) + +rollback; create temp table parent (k int primary key, pd int); create temp table child (k int unique, cd int); insert into parent values (1, 10), (2, 20), (3, 30); diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index 718e1d9..8591050 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -977,6 +977,103 @@ select i8.* from int8_tbl i8 left join (select f1 from int4_tbl group by f1) i4 rollback; +begin work; + +create temp table c ( + id int primary key +); +create temp table b ( + id int primary key, + c_id int not null, + val int not null, + constraint b_c_id_fkey foreign key (c_id) references c deferrable +); +create temp table a ( + id int primary key, + b_id int not null, + constraint a_b_id_fkey foreign key (b_id) references b deferrable +); + +insert into c (id) values(1); +insert into b (id,c_id,val) values(2,1,10); +insert into a (id,b_id) values(3,2); + +-- this should remove inner join to b +explain (costs off) +select a.* from a inner join b on a.b_id = b.id; + +-- this should remove inner join to b and c +explain (costs off) +select a.* from a inner join b on a.b_id = b.id inner join c on b.c_id = c.id; + +-- Ensure all of the target entries have their proper aliases. +select a.* from a inner join b on a.b_id = b.id inner join c on b.c_id = c.id; + +-- change order of tables in query, this should generate the same plan as above. +explain (costs off) +select a.* from c inner join b on c.id = b.c_id inner join a on a.b_id = b.id; + +-- inner join can't be removed due to b columns in the target list +explain (costs off) +select * from a inner join b on a.b_id = b.id; + +-- this should not remove inner join to b due to quals restricting results from b +explain (costs off) +select a.* from a inner join b on a.b_id = b.id where b.val = 10; + +-- check merge join nodes are removed properly +set enable_hashjoin = off; + +-- this should remove joins to b and c. +explain (costs off) +select count(*) from a inner join b on a.b_id = b.id left join c on a.id = c.id; + +-- this should remove joins to b and c, however it b will only be removed on +-- 2nd attempt after c is removed by the left join removal code. +explain (costs off) +select count(*) from a inner join b on a.b_id = b.id left join c on b.id = c.id; + +set enable_hashjoin = on; + +-- this should not remove join to b +explain (costs off) +select a.* from a inner join b on a.b_id = b.id where b.val = b.id; + +-- this should not remove the join, no foreign key exists between a.id and b.id +explain (costs off) +select a.* from a inner join b on a.id = b.id; + +-- ensure a left joined rel can't remove an inner joined rel +explain (costs off) +select a.* from b left join a on b.id = a.b_id; + +-- Ensure we remove b, but don't try and remove c. c has no join condition. +explain (costs off) +select a.* from a inner join b on a.b_id = b.id cross join c; + +set constraints b_c_id_fkey deferred; + +-- join should be removed. +explain (costs off) +select b.* from b inner join c on b.c_id = c.id; + +prepare ab as select b.* from b inner join c on b.c_id = c.id; + +explain (costs off) +execute ab; + +-- perform an update which will cause some pending fk triggers to be added +update c set id = 2 where id=1; + +-- ensure inner join is no longer removed. +explain (costs off) +select b.* from b inner join c on b.c_id = c.id; + +explain (costs off) +execute ab; + +rollback; + create temp table parent (k int primary key, pd int); create temp table child (k int unique, cd int); insert into parent values (1, 10), (2, 20), (3, 30);