diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index b31d892121..36bf47dc8e 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -2618,6 +2618,225 @@ check_redundant_nullability_qual(PlannerInfo *root, Node *clause) return false; } +/* + * add_baserestrictinfo_to_rel + * Add 'restrictinfo' as a baserestrictinfo to the base relation denoted by + * 'relid'. While here, attempt to determine if the given qual is always + * true or always false when evaluated as a base qual. If we can determine + * that the qual needn't be evaluated by the executor, we replace the qual + * with a constant "true" or "false". + */ +static void +add_baserestrictinfo_to_rel(PlannerInfo *root, Index relid, + RestrictInfo *restrictinfo) +{ + RelOptInfo *rel = find_base_rel(root, relid); + Expr *newclause; + + Assert(bms_membership(restrictinfo->required_relids) == BMS_SINGLETON); + + newclause = transform_clause(root, restrictinfo->clause); + + if (newclause != restrictinfo->clause) + { + restrictinfo = make_restrictinfo(root, + newclause, + restrictinfo->is_pushed_down, + restrictinfo->has_clone, + restrictinfo->is_clone, + restrictinfo->pseudoconstant, + restrictinfo->security_level, + restrictinfo->required_relids, + restrictinfo->incompatible_relids, + restrictinfo->outer_relids); + } + + /* Add clause to rel's restriction list */ + rel->baserestrictinfo = lappend(rel->baserestrictinfo, restrictinfo); + + /* Update security level info */ + rel->baserestrict_min_security = Min(rel->baserestrict_min_security, + restrictinfo->security_level); +} + +/* + * transform_nulltest + * Attempt to transform the given NullTest and into a constant "true" or + * "false". This maybe possible when the column has a NOT NULL constraint + * which means an IS NOT NULL clause is always true or a IS NULL clause is + * always false. If there is no NOT NULL constraint or if the column is + * NULLable by an outer join, or the NullTest's arg isn't a Var, then we + * return the input NullTest. + */ +static Expr * +transform_nulltest(PlannerInfo *root, NullTest *nulltest) +{ + Var *var; + bool notnull = false; + + Assert(IsA(nulltest, NullTest)); + + /* + * We only support Vars as there's no ability to have a NOT NULL constraint + * on anything else. + */ + if (!IsA(nulltest->arg, Var)) + return (Expr *) nulltest; + + var = (Var *) nulltest->arg; + + /* could the Var be nulled by any outer joins? */ + if (var->varnullingrels != NULL) + return (Expr *) nulltest; + + /* system columns cannot be NULL */ + if (var->varattno < 0) + notnull = true; + else if (var->varattno > 0) + { + /* is the column defined NOT NULL? */ + RelOptInfo *rel = find_base_rel(root, var->varno); + + if (bms_is_member(var->varattno, rel->notnullattnums)) + notnull = true; + } + else + { + RelOptInfo *rel = find_base_rel(root, var->varno); + Bitmapset *allattnums = bms_add_range(NULL, 1, rel->max_attr); + + /* + * Handle whole-row Vars. The SQL standard defines IS [NOT] NULL for a + * non-null rowtype argument as: + * + * "R IS NULL" is true if every field is the null value. + * + * "R IS NOT NULL" is true if no field is the null value. + */ + + /* + * XXX the IS NOT NULL won't work correctly if there are dropped columns. + * Is it worth having another set in RelOptInfo to allow us to remove + * dropped columns from allattnums before the bms_nonempty_difference? + * XXX should we even bother handling whole-row Vars?? + */ + if (nulltest->nulltesttype == IS_NOT_NULL) + notnull = !bms_nonempty_difference(allattnums, + rel->notnullattnums); + else + notnull = bms_overlap(allattnums, rel->notnullattnums); + bms_free(allattnums); + } + + /* + * When the column cannot be NULL, we can transform IS NOT NULL quals to + * "true" and IS NULL quals to "false". + */ + if (notnull) + { + bool constexpr = (nulltest->nulltesttype == IS_NOT_NULL); + + return (Expr *) makeBoolConst(constexpr, false); + } + + return (Expr *) nulltest; +} + +/* + * transform_clause + * Check and attempt to transform 'clause' into either a constant true or + * constant false if we're able to determine that the qual needn't be + * evaluated by the executor. + * + * Returns a newly allocated Expr if any tranformation was done, else returns + * the input Expr unmodified. + */ +Expr * +transform_clause(PlannerInfo *root, Expr *clause) +{ + /* + * Currently we only check for NullTest quals and OR clauses that include + * NullTest quals. + */ + + /* Check for NullTest qual */ + if (IsA(clause, NullTest)) + return transform_nulltest(root, (NullTest *) clause); + + if (is_orclause(clause)) + { + ListCell *lc; + bool reeval_const = false; + + /* If it's an OR, check its sub-clauses */ + foreach (lc, ((BoolExpr *) clause)->args) + { + Expr *orarg = (Expr *) lfirst(lc); + + if (IsA(orarg, Const)) + reeval_const = true; + else + { + Expr *newexpr = transform_clause(root, orarg); + + if (newexpr != orarg) + { + reeval_const = true; + lfirst(lc) = newexpr; + } + } + } + + /* + * If we managed to transform any clauses or found a Const in the OR clause + * then let's try constant folding again as it may allow us to simplify (or + * delete) the OR clause. + */ + if (reeval_const) + clause = (Expr *) eval_const_expressions(root, (Node *) clause); + } + + return clause; +} + +void +transform_join_clauses(PlannerInfo *root) +{ + if (root->simple_rel_array_size == 1) + return; + + for (int i = 1; i < root->simple_rel_array_size; i++) + { + RelOptInfo *rel = root->simple_rel_array[i]; + ListCell *lc; + + if (rel == NULL || rel->reloptkind != RELOPT_BASEREL) + continue; + + foreach(lc, rel->joininfo) + { + RestrictInfo *restrictinfo = lfirst(lc); + Expr *newclause = transform_clause(root, restrictinfo->clause); + + if (newclause != restrictinfo->clause) + { + restrictinfo = + make_restrictinfo(root, + newclause, + restrictinfo->is_pushed_down, + restrictinfo->has_clone, + restrictinfo->is_clone, + restrictinfo->pseudoconstant, + restrictinfo->security_level, + restrictinfo->required_relids, + restrictinfo->incompatible_relids, + restrictinfo->outer_relids); + lfirst(lc) = restrictinfo; + } + } + } +} + /* * distribute_restrictinfo_to_rels * Push a completed RestrictInfo into the proper restriction or join @@ -2632,58 +2851,39 @@ distribute_restrictinfo_to_rels(PlannerInfo *root, RestrictInfo *restrictinfo) { Relids relids = restrictinfo->required_relids; - RelOptInfo *rel; + int relid; - switch (bms_membership(relids)) + if (relids == NULL) { - case BMS_SINGLETON: - - /* - * There is only one relation participating in the clause, so it - * is a restriction clause for that relation. - */ - rel = find_base_rel(root, bms_singleton_member(relids)); - - /* Add clause to rel's restriction list */ - rel->baserestrictinfo = lappend(rel->baserestrictinfo, - restrictinfo); - /* Update security level info */ - rel->baserestrict_min_security = Min(rel->baserestrict_min_security, - restrictinfo->security_level); - break; - case BMS_MULTIPLE: - - /* - * The clause is a join clause, since there is more than one rel - * in its relid set. - */ - - /* - * Check for hashjoinable operators. (We don't bother setting the - * hashjoin info except in true join clauses.) - */ - check_hashjoinable(restrictinfo); + /* + * clause references no rels, and therefore we have no place to + * attach it. Shouldn't get here if callers are working properly. + */ + elog(ERROR, "cannot cope with variable-free clause"); + } + else if (bms_get_singleton_member(relids, &relid)) + add_baserestrictinfo_to_rel(root, relid, restrictinfo); + else + { + /* + * The clause is a join clause, since there is more than one rel in + * its relid set. + */ - /* - * Likewise, check if the clause is suitable to be used with a - * Memoize node to cache inner tuples during a parameterized - * nested loop. - */ - check_memoizable(restrictinfo); + /* + * Check for hashjoinable operators. (We don't bother setting the + * hashjoin info except in true join clauses.) + */ + check_hashjoinable(restrictinfo); - /* - * Add clause to the join lists of all the relevant relations. - */ - add_join_clause_to_rels(root, restrictinfo, relids); - break; - default: + /* + * Likewise, check if the clause is suitable to be used with a Memoize + * node to cache inner tuples during a parameterized nested loop. + */ + check_memoizable(restrictinfo); - /* - * clause references no rels, and therefore we have no place to - * attach it. Shouldn't get here if callers are working properly. - */ - elog(ERROR, "cannot cope with variable-free clause"); - break; + /* Add clause to the join lists of all the relevant relations. */ + add_join_clause_to_rels(root, restrictinfo, relids); } } diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c index fcc0eacd25..e24887f505 100644 --- a/src/backend/optimizer/plan/planmain.c +++ b/src/backend/optimizer/plan/planmain.c @@ -195,6 +195,8 @@ query_planner(PlannerInfo *root, */ reconsider_outer_join_clauses(root); + transform_join_clauses(root); + /* * If we formed any equivalence classes, generate additional restriction * clauses as appropriate. (Implied join clauses are formed on-the-fly diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index 243c8fb1e4..4677f479c5 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -163,6 +163,23 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, rel->attr_widths = (int32 *) palloc0((rel->max_attr - rel->min_attr + 1) * sizeof(int32)); + /* record which columns are defined as NOT NULL */ + for (int i = 0; i < relation->rd_att->natts; i++) + { + FormData_pg_attribute *attr = &relation->rd_att->attrs[i]; + + if (attr->attnotnull) + { + rel->notnullattnums = bms_add_member(rel->notnullattnums, + attr->attnum); + /* + * Per ATExecDropNotNull(), dropped columns will have their attnotnull + * unset, so we needn't check for dropped columns in the above condition. + */ + Assert(!attr->attisdropped); + } + } + /* * Estimate relation size --- unless it's an inheritance parent, in which * case the size we want is not the rel's own size but the size of its @@ -1549,16 +1566,20 @@ relation_excluded_by_constraints(PlannerInfo *root, /* * Regardless of the setting of constraint_exclusion, detect - * constant-FALSE-or-NULL restriction clauses. Because const-folding will - * reduce "anything AND FALSE" to just "FALSE", any such case should - * result in exactly one baserestrictinfo entry. This doesn't fire very - * often, but it seems cheap enough to be worth doing anyway. (Without - * this, we'd miss some optimizations that 9.5 and earlier found via much - * more roundabout methods.) + * constant-FALSE-or-NULL restriction clauses. Const-folding will reduce + * "anything AND FALSE" to just "FALSE", any such case should result in + * exactly one baserestrictinfo entry. The transform_clause() + * infrastructure isn't quite as careful and may leave a "FALSE" within a + * List of RestrictInfos. Here we loop over each restrictinfo to check + * and return true if we find any impossible RestrictInfos. + * + * This doesn't fire very often, but it seems cheap enough to be worth + * doing anyway. (Without this, we'd miss some optimizations that 9.5 and + * earlier found via much more roundabout methods.) */ - if (list_length(rel->baserestrictinfo) == 1) + foreach(lc, rel->baserestrictinfo) { - RestrictInfo *rinfo = (RestrictInfo *) linitial(rel->baserestrictinfo); + RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc); Expr *clause = rinfo->clause; if (clause && IsA(clause, Const) && diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index 76dad17e33..5d6d97c111 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -221,6 +221,7 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent) rel->relid = relid; rel->rtekind = rte->rtekind; /* min_attr, max_attr, attr_needed, attr_widths are set below */ + rel->notnullattnums = NULL; rel->lateral_vars = NIL; rel->indexlist = NIL; rel->statlist = NIL; @@ -707,6 +708,7 @@ build_join_rel(PlannerInfo *root, joinrel->max_attr = 0; joinrel->attr_needed = NULL; joinrel->attr_widths = NULL; + joinrel->notnullattnums = NULL; joinrel->nulling_relids = NULL; joinrel->lateral_vars = NIL; joinrel->lateral_referencers = NULL; @@ -905,6 +907,7 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel, joinrel->max_attr = 0; joinrel->attr_needed = NULL; joinrel->attr_widths = NULL; + joinrel->notnullattnums = NULL; joinrel->nulling_relids = NULL; joinrel->lateral_vars = NIL; joinrel->lateral_referencers = NULL; diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 5702fbba60..d4f6bbcd2c 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -913,6 +913,8 @@ typedef struct RelOptInfo Relids *attr_needed pg_node_attr(read_write_ignore); /* array indexed [min_attr .. max_attr] */ int32 *attr_widths pg_node_attr(read_write_ignore); + /* zero-based set containing attnums of NOT NULL columns */ + Bitmapset *notnullattnums; /* relids of outer joins that can null this baserel */ Relids nulling_relids; /* LATERAL Vars and PHVs referenced by rel */ diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 31c188176b..f8b794b0e3 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -75,6 +75,8 @@ extern void add_vars_to_targetlist(PlannerInfo *root, List *vars, extern void find_lateral_references(PlannerInfo *root); extern void create_lateral_join_info(PlannerInfo *root); extern List *deconstruct_jointree(PlannerInfo *root); +extern Expr *transform_clause(PlannerInfo *root, Expr *clause); +extern void transform_join_clauses(PlannerInfo *root); extern void distribute_restrictinfo_to_rels(PlannerInfo *root, RestrictInfo *restrictinfo); extern RestrictInfo *process_implied_equality(PlannerInfo *root, diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 9b8638f286..b95d30f658 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -5788,13 +5788,11 @@ explain (costs off) select p.* from parent p left join child c on (p.k = c.k) where p.k = 1 and p.k = 2; - QUERY PLAN ------------------------------------------------- + QUERY PLAN +-------------------------- Result One-Time Filter: false - -> Index Scan using parent_pkey on parent p - Index Cond: (k = 1) -(4 rows) +(2 rows) select p.* from (parent p left join child c on (p.k = c.k)) join parent x on p.k = x.k diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out new file mode 100644 index 0000000000..5c4914dc80 --- /dev/null +++ b/src/test/regress/expected/predicate.out @@ -0,0 +1,146 @@ +-- +-- Tests for predicate transformation +-- +CREATE TABLE pred_tab (a INT NOT NULL, b INT); +-- +-- test that restrictions that we detect as always true are ignored +-- +-- An IS NOT NULL qual in restriction clauses can be ignored if it's on a NOT +-- NULL column +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.a IS NOT NULL; + QUERY PLAN +------------------------ + Seq Scan on pred_tab t +(1 row) + +-- A more complex variant of the above. Ensure we're left only with the +-- t.b = 1 qual since t.a IS NOT NULL is always true +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.b = 1 AND t.a IS NOT NULL; + QUERY PLAN +------------------------ + Seq Scan on pred_tab t + Filter: (b = 1) +(2 rows) + +-- Ensure t.b IS NOT NULL is not removed as t.b allows NULL values. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.b IS NOT NULL; + QUERY PLAN +--------------------------- + Seq Scan on pred_tab t + Filter: (b IS NOT NULL) +(2 rows) + +-- Ensure the t.a IS NOT NULL is detected as always true. We shouldn't see +-- any quals in the scan. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.a IS NOT NULL OR t.b = 1; + QUERY PLAN +------------------------ + Seq Scan on pred_tab t +(1 row) + +-- Ensure the quals remain as t.b allows NULL values. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.b IS NOT NULL OR t.a = 1; + QUERY PLAN +---------------------------------------- + Seq Scan on pred_tab t + Filter: ((b IS NOT NULL) OR (a = 1)) +(2 rows) + +-- An IS NOT NULL NullTest in join clauses can be ignored if +-- a) it's on a NOT NULL column, and; +-- b) its Var is not nulled by any outer joins +-- Ensure t2.a IS NOT NULL is not seen in the plan +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON true +LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL; + QUERY PLAN +------------------------------------------------- + Nested Loop Left Join + -> Seq Scan on pred_tab t1 + -> Materialize + -> Nested Loop Left Join + -> Seq Scan on pred_tab t2 + -> Materialize + -> Seq Scan on pred_tab t3 +(7 rows) + +-- When some t2 rows are missing due to the left join we cannot forego +-- including the t2.a is not null in the plan. Ensure it remains. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON t1.a = 1 +LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL; + QUERY PLAN +------------------------------------------- + Nested Loop Left Join + Join Filter: (t2.a IS NOT NULL) + -> Nested Loop Left Join + Join Filter: (t1.a = 1) + -> Seq Scan on pred_tab t1 + -> Materialize + -> Seq Scan on pred_tab t2 + -> Materialize + -> Seq Scan on pred_tab t3 +(9 rows) + +-- Ensure t2.a IS NULL is detected as constantly TRUE and results in none of +-- the quals appearing in the plan. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON true +LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b = 1; + QUERY PLAN +------------------------------------------------- + Nested Loop Left Join + -> Seq Scan on pred_tab t1 + -> Materialize + -> Nested Loop Left Join + -> Seq Scan on pred_tab t2 + -> Materialize + -> Seq Scan on pred_tab t3 +(7 rows) + +-- Ensure that the IS NOT NULL qual isn't removed as t2.a is nullable from +-- t2's left join to t1. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON t1.a = 1 +LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b = 1; + QUERY PLAN +--------------------------------------------------- + Nested Loop Left Join + Join Filter: ((t2.a IS NOT NULL) OR (t2.b = 1)) + -> Nested Loop Left Join + Join Filter: (t1.a = 1) + -> Seq Scan on pred_tab t1 + -> Materialize + -> Seq Scan on pred_tab t2 + -> Materialize + -> Seq Scan on pred_tab t3 +(9 rows) + +-- +-- Ensure that impossible IS NULL NullTests are detected when the Var cannot +-- be NULL +-- +-- Ensure we detect t.a IS NULL is impossible AND forego the scan +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.a IS NULL; + QUERY PLAN +-------------------------- + Result + One-Time Filter: false +(2 rows) + +-- As above, but add an additional qual +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.a = 1234 AND t.a IS NULL; + QUERY PLAN +-------------------------- + Result + One-Time Filter: false +(2 rows) + +DROP TABLE pred_tab; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 4df9d8503b..3816efc7b3 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -119,7 +119,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr # The stats test resets stats, so nothing else needing stats access can be in # this group. # ---------- -test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats +test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate # event_trigger depends on create_am and cannot run concurrently with # any test that runs DDL diff --git a/src/test/regress/sql/predicate.sql b/src/test/regress/sql/predicate.sql new file mode 100644 index 0000000000..faeb54aee7 --- /dev/null +++ b/src/test/regress/sql/predicate.sql @@ -0,0 +1,75 @@ +-- +-- Tests for predicate transformation +-- + +CREATE TABLE pred_tab (a INT NOT NULL, b INT); + +-- +-- test that restrictions that we detect as always true are ignored +-- + +-- An IS NOT NULL qual in restriction clauses can be ignored if it's on a NOT +-- NULL column +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.a IS NOT NULL; + +-- A more complex variant of the above. Ensure we're left only with the +-- t.b = 1 qual since t.a IS NOT NULL is always true +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.b = 1 AND t.a IS NOT NULL; + +-- Ensure t.b IS NOT NULL is not removed as t.b allows NULL values. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.b IS NOT NULL; + +-- Ensure the t.a IS NOT NULL is detected as always true. We shouldn't see +-- any quals in the scan. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.a IS NOT NULL OR t.b = 1; + +-- Ensure the quals remain as t.b allows NULL values. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.b IS NOT NULL OR t.a = 1; + +-- An IS NOT NULL NullTest in join clauses can be ignored if +-- a) it's on a NOT NULL column, and; +-- b) its Var is not nulled by any outer joins + +-- Ensure t2.a IS NOT NULL is not seen in the plan +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON true +LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL; + +-- When some t2 rows are missing due to the left join we cannot forego +-- including the t2.a is not null in the plan. Ensure it remains. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON t1.a = 1 +LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL; + +-- Ensure t2.a IS NULL is detected as constantly TRUE and results in none of +-- the quals appearing in the plan. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON true +LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b = 1; + +-- Ensure that the IS NOT NULL qual isn't removed as t2.a is nullable from +-- t2's left join to t1. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON t1.a = 1 +LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b = 1; + +-- +-- Ensure that impossible IS NULL NullTests are detected when the Var cannot +-- be NULL +-- + +-- Ensure we detect t.a IS NULL is impossible AND forego the scan +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.a IS NULL; + +-- As above, but add an additional qual +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t WHERE t.a = 1234 AND t.a IS NULL; + +DROP TABLE pred_tab; +