not_in_anti_join_v0.9.delta.patch
application/octet-stream
Filename: not_in_anti_join_v0.9.delta.patch
Type: application/octet-stream
Part: 2
Patch
Format: unified
Series: patch v0
| File | + | − |
|---|---|---|
| src/backend/optimizer/plan/subselect.c | 117 | 3 |
| src/backend/optimizer/util/clauses.c | 8 | 13 |
| src/include/optimizer/clauses.h | 1 | 1 |
| src/test/regress/expected/subselect.out | 90 | 19 |
| src/test/regress/sql/subselect.sql | 37 | 9 |
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index 4b44662..6ebab4d 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -70,6 +70,8 @@ static Node *convert_testexpr_mutator(Node *node,
static bool subplan_is_hashable(Plan *plan);
static bool testexpr_is_hashable(Node *testexpr);
static bool hash_ok_operator(OpExpr *expr);
+static bool is_NOTANY_compatible_with_antijoin(Query *outerquery,
+ SubLink *sublink);
static bool simplify_EXISTS_query(Query *query);
static Query *convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect,
Node **testexpr, List **paramIds);
@@ -1187,6 +1189,117 @@ SS_process_ctes(PlannerInfo *root)
}
/*
+ * is_NOTANY_compatible_with_antijoin
+ * True if the NOT IN sublink can be safely converted into an ANTI JOIN.
+ * Per SQL spec, NOT IN is not ordinarily equivalent to an anti-join,
+ * however, if we can prove that all of the expressions on both sides of
+ * the would be join condition are all certainly not NULL, then it's safe
+ * to convert NOT IN to an anti-join.
+ *
+ * Note: This function is quite locked into the NOT IN syntax. Certain
+ * assumptions are made about the structure of the join conditions:
+ *
+ * 1. We assume that when more than 1 join condition exists that these are AND
+ * type conditions, i.e not OR conditions.
+ *
+ * 2. We assume that each join qual has 2 arguments and the first arguments in
+ * each join qual is the one that belongs to the outer side of the NOT IN
+ * clause.
+ */
+static bool
+is_NOTANY_compatible_with_antijoin(Query *outerquery, SubLink *sublink)
+{
+ ListCell *lc;
+ List *outerexpr;
+ List *innerexpr;
+ Node *testexpr = sublink->testexpr;
+ Query *subselect = (Query *) sublink->subselect;
+
+ /*
+ * We must build 2 lists of expressions, one for the outer query side and
+ * one for the subquery/inner side of the NOT IN clause. It is these lists
+ * that we pass to to expressions_are_not_nullable to allow it to determine
+ * if each expression cannot contain NULL values or not. We'll try and be
+ * as lazy about this as possible and we won't bother generating the 2nd
+ * list if the first list can't be proved to be free from null-able
+ * expressions. The order that we checks these lists does not really matter
+ * as neither one seems to be more likely to allow us to exit from this
+ * function more quickly.
+ */
+
+ /* if it's a single expression */
+ if (IsA(testexpr, OpExpr))
+ {
+ OpExpr *opexpr = (OpExpr *) testexpr;
+
+ Assert(list_length(opexpr->args) == 2);
+
+ outerexpr = lappend(NIL, linitial(opexpr->args));
+ }
+
+ /* multiple expressions ANDed together */
+ else if (IsA(testexpr, BoolExpr))
+ {
+ List *list = ((BoolExpr *) testexpr)->args;
+
+ outerexpr = NIL;
+
+ /* loop through each expression appending to the list each iteration. */
+ foreach(lc, list)
+ {
+ OpExpr *opexpr = (OpExpr *) lfirst(lc);
+
+ Assert(list_length(opexpr->args) == 2);
+
+ outerexpr = lappend(outerexpr, linitial(opexpr->args));
+ }
+ }
+ else
+ elog(ERROR, "unrecognized node type: %d",
+ (int) nodeTag(testexpr));
+
+ if (outerexpr == NIL)
+ elog(ERROR, "unexpected empty clause");
+
+ /*
+ * Check the expressions being used in the outer query side of the NOT IN
+ * clause to ensure NULLs are not possible.
+ */
+ if (!expressions_are_not_nullable(outerquery, outerexpr))
+ return false;
+
+ /*
+ * Now we check to ensure each TargetEntry in the subquery can be proved to
+ * never be NULL.
+ */
+
+ innerexpr = NIL;
+
+ foreach(lc, subselect->targetList)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+ /* Resjunk columns can be ignored: they don't produce output values */
+ if (tle->resjunk)
+ continue;
+
+ innerexpr = lappend(innerexpr, tle->expr);
+ }
+
+ if (innerexpr == NIL)
+ elog(ERROR, "unexpected empty clause");
+
+ /*
+ * Check if all the subquery's targetlist columns can be proved to be not
+ * null.
+ */
+ if (!expressions_are_not_nullable(subselect, innerexpr))
+ return false;
+
+ return true; /* supports ANTI JOIN */
+}
+
+/*
* convert_ANY_sublink_to_join: try to convert an ANY SubLink to a join
*
* The caller has found an ANY SubLink at the top level of one of the query's
@@ -1242,10 +1355,11 @@ convert_ANY_sublink_to_join(PlannerInfo *root, SubLink *sublink,
/*
* Per SQL spec, NOT IN is not ordinarily equivalent to an anti-join, so
* that by default we have to fail when under_not. However, if we can
- * prove that the sub-select's output columns are all certainly not NULL,
- * then it's safe to convert NOT IN to an anti-join.
+ * prove that all of the expressions on both sides of the, would be, join
+ * condition are all certainly not NULL, then it's safe to convert NOT IN
+ * to an anti-join.
*/
- if (under_not && !query_outputs_are_not_nullable(subselect))
+ if (under_not && !is_NOTANY_compatible_with_antijoin(parse, sublink))
return NULL;
/*
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index f8e3eaa..dd02327 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -1994,9 +1994,9 @@ find_forced_null_var(Node *node)
}
/*
- * query_outputs_are_not_nullable
- * Returns TRUE if the output values of the Query are certainly not NULL.
- * All output columns must return non-NULL to answer TRUE.
+ * expressions_are_not_nullable
+ * Returns TRUE if each Expr in the expression list is certainly not NULL.
+ * All Exprs must return non-NULL to answer TRUE.
*
* The reason this takes a Query, and not just an individual tlist expression,
* is so that we can make use of the query's WHERE/ON clauses to prove it does
@@ -2012,7 +2012,7 @@ find_forced_null_var(Node *node)
* side of conservatism: if we're not sure, it's okay to return FALSE.
*/
bool
-query_outputs_are_not_nullable(Query *query)
+expressions_are_not_nullable(Query *query, List *exprlist)
{
PlannerInfo subroot;
Relids innerjoined_rels = NULL;
@@ -2020,7 +2020,7 @@ query_outputs_are_not_nullable(Query *query)
List *usable_quals = NIL;
List *nonnullable_vars = NIL;
bool computed_nonnullable_vars = false;
- ListCell *tl;
+ ListCell *lc;
/*
* If the query contains set operations, punt. The set ops themselves
@@ -2044,16 +2044,11 @@ query_outputs_are_not_nullable(Query *query)
subroot.parse = query;
/*
- * Examine each targetlist entry to prove that it can't produce NULL.
+ * Examine each Expr to prove that it can't produce NULL.
*/
- foreach(tl, query->targetList)
+ foreach(lc, exprlist)
{
- TargetEntry *tle = (TargetEntry *) lfirst(tl);
- Expr *expr = tle->expr;
-
- /* Resjunk columns can be ignored: they don't produce output values */
- if (tle->resjunk)
- continue;
+ Expr *expr = (Expr *) lfirst(lc);
/*
* For the most part we don't try to deal with anything more complex
diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h
index 5b25d01..90f745c 100644
--- a/src/include/optimizer/clauses.h
+++ b/src/include/optimizer/clauses.h
@@ -69,7 +69,7 @@ extern Relids find_nonnullable_rels(Node *clause);
extern List *find_nonnullable_vars(Node *clause);
extern List *find_forced_null_vars(Node *clause);
extern Var *find_forced_null_var(Node *clause);
-extern bool query_outputs_are_not_nullable(Query *query);
+extern bool expressions_are_not_nullable(Query *query, List *exprlist);
extern bool is_pseudo_constant_clause(Node *clause);
extern bool is_pseudo_constant_clause_relids(Node *clause, Relids relids);
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index b63f7ac..610c175 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -805,24 +805,96 @@ select nextval('ts1');
--
-- Check NOT IN performs an ANTI JOIN when NULLs are not possible
--- in the target list of the subquery.
+-- on either side of the, would be, join condition.
--
BEGIN;
CREATE TEMP TABLE a (id INT PRIMARY KEY);
CREATE TEMP TABLE b (x INT NOT NULL, y INT);
CREATE TEMP TABLE c (z INT NOT NULL);
--- ANTI JOIN. x is defined as NOT NULL
+INSERT INTO b VALUES(1,1),(2,2),(3,NULL);
+-- No ANTI JOIN, b.x is from an outer join
EXPLAIN (COSTS OFF)
-SELECT * FROM a WHERE id NOT IN (SELECT x FROM b);
- QUERY PLAN
------------------------------------------
- Merge Anti Join
- Merge Cond: (a.id = b.x)
- -> Index Only Scan using a_pkey on a
- -> Sort
- Sort Key: b.x
+SELECT * FROM a
+LEFT OUTER JOIN b ON a.id = b.x
+WHERE b.x NOT IN(SELECT z FROM c);
+ QUERY PLAN
+------------------------------------
+ Hash Right Join
+ Hash Cond: (b.x = a.id)
+ Filter: (NOT (hashed SubPlan 1))
+ -> Seq Scan on b
+ -> Hash
+ -> Seq Scan on a
+ SubPlan 1
+ -> Seq Scan on c
+(8 rows)
+
+-- ANTI JOIN, b.x is from an outer join but b.x > 100
+-- forces the join not to produce NULL on the righthand
+-- side.
+EXPLAIN (COSTS OFF)
+SELECT * FROM a
+LEFT OUTER JOIN b ON a.id = b.x
+WHERE b.x NOT IN(SELECT z FROM c)
+ AND b.x > 100;
+ QUERY PLAN
+---------------------------------
+ Hash Join
+ Hash Cond: (b.x = a.id)
+ -> Hash Anti Join
+ Hash Cond: (b.x = c.z)
-> Seq Scan on b
-(6 rows)
+ Filter: (x > 100)
+ -> Hash
+ -> Seq Scan on c
+ -> Hash
+ -> Seq Scan on a
+(10 rows)
+
+-- No ANTI JOIN, b.x is from an outer join
+EXPLAIN (COSTS OFF)
+SELECT * FROM a
+FULL OUTER JOIN b ON a.id = b.x
+WHERE b.x NOT IN(SELECT y FROM c);
+ QUERY PLAN
+-----------------------------
+ Hash Full Join
+ Hash Cond: (b.x = a.id)
+ Filter: (NOT (SubPlan 1))
+ -> Seq Scan on b
+ -> Hash
+ -> Seq Scan on a
+ SubPlan 1
+ -> Seq Scan on c
+(8 rows)
+
+-- No ANTI JOIN. y can have NULLs
+EXPLAIN (COSTS OFF)
+SELECT * FROM b WHERE y NOT IN (SELECT z FROM c);
+ QUERY PLAN
+------------------------------------
+ Seq Scan on b
+ Filter: (NOT (hashed SubPlan 1))
+ SubPlan 1
+ -> Seq Scan on c
+(4 rows)
+
+-- c is an empty relation so should cause no filtering on b
+SELECT * FROM b WHERE y NOT IN (SELECT z FROM c);
+ x | y
+---+---
+ 1 | 1
+ 2 | 2
+ 3 |
+(3 rows)
+
+INSERT INTO c VALUES(1);
+-- Records where y is NULL should be filtered out.
+SELECT * FROM b WHERE y NOT IN (SELECT z FROM c);
+ x | y
+---+---
+ 2 | 2
+(1 row)
-- No ANTI JOIN, y can be NULL
EXPLAIN (COSTS OFF)
@@ -988,7 +1060,7 @@ SELECT * FROM a WHERE id NOT IN (SELECT b.x FROM b RIGHT JOIN c ON b.x = c.z);
-> Seq Scan on c
(11 rows)
--- No ANTI JOIN, c.z is not from an outer join
+-- No ANTI JOIN, c.z is from an outer join
EXPLAIN (COSTS OFF)
SELECT * FROM a WHERE id NOT IN (SELECT c.z FROM b FULL JOIN c ON b.x = c.z);
QUERY PLAN
@@ -1080,23 +1152,22 @@ SELECT * FROM a WHERE id NOT IN (SELECT c.z FROM b LEFT JOIN c ON b.x = c.z WHER
Filter: (z IS NOT NULL)
(13 rows)
-ALTER TABLE c ADD COLUMN x INT;
--- ANTI JOIN, x cannot be NULL as b.x has a NOT NULL constraint
+-- ANTI JOIN, b.y cannot be NULL due to the join condition b.y = c.z
EXPLAIN (COSTS OFF)
-SELECT * FROM a WHERE id NOT IN (SELECT x FROM b NATURAL JOIN c);
+SELECT * FROM a WHERE id NOT IN (SELECT b.y FROM b INNER JOIN c ON b.y = c.z);
QUERY PLAN
-----------------------------------------
Merge Anti Join
- Merge Cond: (a.id = b.x)
+ Merge Cond: (a.id = b.y)
-> Index Only Scan using a_pkey on a
-> Materialize
-> Merge Join
- Merge Cond: (b.x = c.x)
+ Merge Cond: (b.y = c.z)
-> Sort
- Sort Key: b.x
+ Sort Key: b.y
-> Seq Scan on b
-> Sort
- Sort Key: c.x
+ Sort Key: c.z
-> Seq Scan on c
(12 rows)
diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql
index c3ca67f..c4f9102 100644
--- a/src/test/regress/sql/subselect.sql
+++ b/src/test/regress/sql/subselect.sql
@@ -447,7 +447,7 @@ select nextval('ts1');
--
-- Check NOT IN performs an ANTI JOIN when NULLs are not possible
--- in the target list of the subquery.
+-- on either side of the, would be, join condition.
--
BEGIN;
@@ -456,9 +456,40 @@ CREATE TEMP TABLE a (id INT PRIMARY KEY);
CREATE TEMP TABLE b (x INT NOT NULL, y INT);
CREATE TEMP TABLE c (z INT NOT NULL);
--- ANTI JOIN. x is defined as NOT NULL
+INSERT INTO b VALUES(1,1),(2,2),(3,NULL);
+
+-- No ANTI JOIN, b.x is from an outer join
+EXPLAIN (COSTS OFF)
+SELECT * FROM a
+LEFT OUTER JOIN b ON a.id = b.x
+WHERE b.x NOT IN(SELECT z FROM c);
+
+-- ANTI JOIN, b.x is from an outer join but b.x > 100
+-- forces the join not to produce NULL on the righthand
+-- side.
+EXPLAIN (COSTS OFF)
+SELECT * FROM a
+LEFT OUTER JOIN b ON a.id = b.x
+WHERE b.x NOT IN(SELECT z FROM c)
+ AND b.x > 100;
+
+-- No ANTI JOIN, b.x is from an outer join
EXPLAIN (COSTS OFF)
-SELECT * FROM a WHERE id NOT IN (SELECT x FROM b);
+SELECT * FROM a
+FULL OUTER JOIN b ON a.id = b.x
+WHERE b.x NOT IN(SELECT y FROM c);
+
+-- No ANTI JOIN. y can have NULLs
+EXPLAIN (COSTS OFF)
+SELECT * FROM b WHERE y NOT IN (SELECT z FROM c);
+
+-- c is an empty relation so should cause no filtering on b
+SELECT * FROM b WHERE y NOT IN (SELECT z FROM c);
+
+INSERT INTO c VALUES(1);
+
+-- Records where y is NULL should be filtered out.
+SELECT * FROM b WHERE y NOT IN (SELECT z FROM c);
-- No ANTI JOIN, y can be NULL
EXPLAIN (COSTS OFF)
@@ -508,7 +539,7 @@ SELECT * FROM a WHERE id NOT IN (SELECT c.z FROM b RIGHT JOIN c ON b.x = c.z);
EXPLAIN (COSTS OFF)
SELECT * FROM a WHERE id NOT IN (SELECT b.x FROM b RIGHT JOIN c ON b.x = c.z);
--- No ANTI JOIN, c.z is not from an outer join
+-- No ANTI JOIN, c.z is from an outer join
EXPLAIN (COSTS OFF)
SELECT * FROM a WHERE id NOT IN (SELECT c.z FROM b FULL JOIN c ON b.x = c.z);
@@ -528,11 +559,8 @@ SELECT * FROM a WHERE id NOT IN (SELECT c.z FROM b LEFT JOIN c ON b.x = c.z WHER
EXPLAIN (COSTS OFF)
SELECT * FROM a WHERE id NOT IN (SELECT c.z FROM b LEFT JOIN c ON b.x = c.z WHERE c.z IS NOT NULL);
-ALTER TABLE c ADD COLUMN x INT;
-
--- ANTI JOIN, x cannot be NULL as b.x has a NOT NULL constraint
+-- ANTI JOIN, b.y cannot be NULL due to the join condition b.y = c.z
EXPLAIN (COSTS OFF)
-SELECT * FROM a WHERE id NOT IN (SELECT x FROM b NATURAL JOIN c);
-
+SELECT * FROM a WHERE id NOT IN (SELECT b.y FROM b INNER JOIN c ON b.y = c.z);
ROLLBACK;