From c8cdd4dac33ce4800dfc094d8985866f891563f8 Mon Sep 17 00:00:00 2001 From: David Rowley Date: Tue, 28 Nov 2023 16:48:08 +1300 Subject: [PATCH v8] Ignore redundant NOT NULL clauses --- .../postgres_fdw/expected/postgres_fdw.out | 16 +- contrib/postgres_fdw/sql/postgres_fdw.sql | 4 +- src/backend/optimizer/plan/initsplan.c | 184 ++++++++++++++-- src/backend/optimizer/util/joininfo.c | 25 +++ src/backend/optimizer/util/plancat.c | 9 + src/backend/optimizer/util/relnode.c | 3 + src/include/nodes/pathnodes.h | 2 + src/include/optimizer/planmain.h | 4 + src/test/regress/expected/equivclass.out | 18 +- src/test/regress/expected/join.out | 52 +++-- src/test/regress/expected/predicate.out | 204 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/predicate.sql | 80 +++++++ 13 files changed, 540 insertions(+), 63 deletions(-) create mode 100644 src/test/regress/expected/predicate.out create mode 100644 src/test/regress/sql/predicate.sql diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 22cae37a1e..a0dd41894a 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -656,20 +656,20 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 100 AND t1.c2 = Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" = 100)) AND ((c2 = 0)) (3 rows) -EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 IS NULL; -- NullTest - QUERY PLAN -------------------------------------------------------------------------------------------------- +EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c3 IS NULL; -- NullTest + QUERY PLAN +---------------------------------------------------------------------------------------------- Foreign Scan on public.ft1 t1 Output: c1, c2, c3, c4, c5, c6, c7, c8 - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" IS NULL)) + Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((c3 IS NULL)) (3 rows) -EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL; -- NullTest - QUERY PLAN ------------------------------------------------------------------------------------------------------ +EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c3 IS NOT NULL; -- NullTest + QUERY PLAN +-------------------------------------------------------------------------------------------------- Foreign Scan on public.ft1 t1 Output: c1, c2, c3, c4, c5, c6, c7, c8 - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" IS NOT NULL)) + Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((c3 IS NOT NULL)) (3 rows) EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE round(abs(c1), 0) = 1; -- FuncExpr diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 075da4ff86..3a78552f19 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -332,8 +332,8 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft_empty ORDER BY c1; -- =================================================================== EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 1; -- Var, OpExpr(b), Const EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 100 AND t1.c2 = 0; -- BoolExpr -EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 IS NULL; -- NullTest -EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL; -- NullTest +EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c3 IS NULL; -- NullTest +EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c3 IS NOT NULL; -- NullTest EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE round(abs(c1), 0) = 1; -- FuncExpr EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 = -c1; -- OpExpr(l) EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE (c1 IS NOT NULL) IS DISTINCT FROM (c1 IS NOT NULL); -- DistinctExpr diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index 8295e7753d..57f1323049 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -2618,6 +2618,174 @@ check_redundant_nullability_qual(PlannerInfo *root, Node *clause) return false; } +/* + * add_base_clause_to_rel + * Add 'restrictinfo' as a baserestrictinfo to the base relation denoted + * by 'relid' with some prechecks to try to determine if the qual is + * always true, in which case we ignore it rather than add it, or if the + * qual is always false, in which case we replace it with constant-FALSE. + */ +static void +add_base_clause_to_rel(PlannerInfo *root, Index relid, + RestrictInfo *restrictinfo) +{ + RelOptInfo *rel = find_base_rel(root, relid); + + Assert(bms_membership(restrictinfo->required_relids) == BMS_SINGLETON); + + /* Don't add the clause if it is always true */ + if (restriction_is_always_true(root, restrictinfo)) + return; + + /* Substitute constant-FALSE for the origin qual if it is always false */ + if (restriction_is_always_false(root, restrictinfo)) + { + int save_rinfo_serial = restrictinfo->rinfo_serial; + + restrictinfo = make_restrictinfo(root, + (Expr *) makeBoolConst(false, false), + restrictinfo->is_pushed_down, + restrictinfo->has_clone, + restrictinfo->is_clone, + restrictinfo->pseudoconstant, + 0, /* security_level */ + restrictinfo->required_relids, + restrictinfo->incompatible_relids, + restrictinfo->outer_relids); + restrictinfo->rinfo_serial = save_rinfo_serial; + } + + /* 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); +} + +/* + * expr_is_nonnullable + * Check to see if the Expr cannot be NULL + * + * If the Expr is a simple Var that is defined NOT NULL and meanwhile is not + * nulled by any outer joins, then we can know that it cannot be NULL. + */ +static bool +expr_is_nonnullable(PlannerInfo *root, Expr *expr) +{ + RelOptInfo *rel; + Var *var; + + /* For now only check simple Vars */ + if (!IsA(expr, Var)) + return false; + + var = (Var *) expr; + + /* could the Var be nulled by any outer joins? */ + if (!bms_is_empty(var->varnullingrels)) + return false; + + /* system columns cannot be NULL */ + if (var->varattno < 0) + return true; + + /* is the column defined NOT NULL? */ + rel = find_base_rel(root, var->varno); + if (var->varattno > 0 && + bms_is_member(var->varattno, rel->notnullattnums)) + return true; + + return false; +} + +/* + * restriction_is_always_true + * Check to see if the RestrictInfo is always true. + * + * Currently we only check for NullTest quals and OR clauses that include + * NullTest quals. We may extend it in the future. + */ +bool +restriction_is_always_true(PlannerInfo *root, + RestrictInfo *restrictinfo) +{ + /* Check for NullTest qual */ + if (IsA(restrictinfo->clause, NullTest)) + { + NullTest *nulltest = (NullTest *) restrictinfo->clause; + + /* is this NullTest an IS_NOT_NULL qual? */ + if (nulltest->nulltesttype != IS_NOT_NULL) + return false; + + return expr_is_nonnullable(root, nulltest->arg); + } + + /* If it's an OR, check its sub-clauses */ + if (restriction_is_or_clause(restrictinfo)) + { + ListCell *lc; + + Assert(is_orclause(restrictinfo->orclause)); + foreach(lc, ((BoolExpr *) restrictinfo->orclause)->args) + { + Node *orarg = (Node *) lfirst(lc); + + if (!IsA(orarg, RestrictInfo)) + continue; + + if (restriction_is_always_true(root, (RestrictInfo *) orarg)) + return true; + } + } + + return false; +} + +/* + * restriction_is_always_false + * Check to see if the RestrictInfo is always false. + * + * Currently we only check for NullTest quals and OR clauses that include + * NullTest quals. We may extend it in the future. + */ +bool +restriction_is_always_false(PlannerInfo *root, + RestrictInfo *restrictinfo) +{ + /* Check for NullTest qual */ + if (IsA(restrictinfo->clause, NullTest)) + { + NullTest *nulltest = (NullTest *) restrictinfo->clause; + + /* is this NullTest an IS_NULL qual? */ + if (nulltest->nulltesttype != IS_NULL) + return false; + + return expr_is_nonnullable(root, nulltest->arg); + } + + /* If it's an OR, check its sub-clauses */ + if (restriction_is_or_clause(restrictinfo)) + { + ListCell *lc; + + Assert(is_orclause(restrictinfo->orclause)); + foreach(lc, ((BoolExpr *) restrictinfo->orclause)->args) + { + Node *orarg = (Node *) lfirst(lc); + + if (!IsA(orarg, RestrictInfo) || + !restriction_is_always_false(root, (RestrictInfo *) orarg)) + return false; + } + return true; + } + + return false; +} + /* * distribute_restrictinfo_to_rels * Push a completed RestrictInfo into the proper restriction or join @@ -2632,27 +2800,13 @@ distribute_restrictinfo_to_rels(PlannerInfo *root, RestrictInfo *restrictinfo) { Relids relids = restrictinfo->required_relids; - RelOptInfo *rel; if (!bms_is_empty(relids)) { int relid; if (bms_get_singleton_member(relids, &relid)) - { - /* - * There is only one relation participating in the clause, so it - * is a restriction clause for that relation. - */ - rel = find_base_rel(root, relid); - - /* 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); - } + add_base_clause_to_rel(root, relid, restrictinfo); else { /* diff --git a/src/backend/optimizer/util/joininfo.c b/src/backend/optimizer/util/joininfo.c index 968a5a488e..d79d6f8169 100644 --- a/src/backend/optimizer/util/joininfo.c +++ b/src/backend/optimizer/util/joininfo.c @@ -14,9 +14,12 @@ */ #include "postgres.h" +#include "nodes/makefuncs.h" #include "optimizer/joininfo.h" #include "optimizer/pathnode.h" #include "optimizer/paths.h" +#include "optimizer/planmain.h" +#include "optimizer/restrictinfo.h" /* @@ -98,6 +101,28 @@ add_join_clause_to_rels(PlannerInfo *root, { int cur_relid; + /* Don't add the clause if it is always true */ + if (restriction_is_always_true(root, restrictinfo)) + return; + + /* Substitute constant-FALSE for the origin qual if it is always false */ + if (restriction_is_always_false(root, restrictinfo)) + { + int save_rinfo_serial = restrictinfo->rinfo_serial; + + restrictinfo = make_restrictinfo(root, + (Expr *) makeBoolConst(false, false), + restrictinfo->is_pushed_down, + restrictinfo->has_clone, + restrictinfo->is_clone, + restrictinfo->pseudoconstant, + 0, /* security_level */ + restrictinfo->required_relids, + restrictinfo->incompatible_relids, + restrictinfo->outer_relids); + restrictinfo->rinfo_serial = save_rinfo_serial; + } + cur_relid = -1; while ((cur_relid = bms_next_member(join_relids, cur_relid)) >= 0) { diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index 7159c775fb..0024b27edc 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -163,6 +163,15 @@ 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); + } + /* * 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 diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index 5d83f60eb9..4edccd6da6 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; @@ -705,6 +706,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; @@ -903,6 +905,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 ed85dc7414..0d5ff34fb2 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 2bc857745a..a4edd2f61d 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -76,6 +76,10 @@ 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 bool restriction_is_always_true(PlannerInfo *root, + RestrictInfo *restrictinfo); +extern bool restriction_is_always_false(PlannerInfo *root, + RestrictInfo *restrictinfo); extern void distribute_restrictinfo_to_rels(PlannerInfo *root, RestrictInfo *restrictinfo); extern RestrictInfo *process_implied_equality(PlannerInfo *root, diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out index de71441052..3d5de28354 100644 --- a/src/test/regress/expected/equivclass.out +++ b/src/test/regress/expected/equivclass.out @@ -438,15 +438,14 @@ set enable_mergejoin to off; explain (costs off) select * from ec0 m join ec0 n on m.ff = n.ff join ec1 p on m.ff + n.ff = p.f1; - QUERY PLAN ----------------------------------------- + QUERY PLAN +--------------------------------------- Nested Loop Join Filter: ((n.ff + n.ff) = p.f1) - -> Seq Scan on ec1 p + -> Seq Scan on ec0 n -> Materialize - -> Seq Scan on ec0 n - Filter: (ff IS NOT NULL) -(6 rows) + -> Seq Scan on ec1 p +(5 rows) explain (costs off) select * from ec0 m join ec0 n on m.ff = n.ff @@ -455,11 +454,10 @@ explain (costs off) --------------------------------------------------------------- Nested Loop Join Filter: ((p.f1)::bigint = ((n.ff + n.ff))::int8alias1) - -> Seq Scan on ec1 p + -> Seq Scan on ec0 n -> Materialize - -> Seq Scan on ec0 n - Filter: (ff IS NOT NULL) -(6 rows) + -> Seq Scan on ec1 p +(5 rows) reset enable_mergejoin; -- this could be converted, but isn't at present diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 2c73270143..24d3004095 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -6357,14 +6357,14 @@ SELECT * FROM pg_am am WHERE am.amname IN ( JOIN pg_class c2 ON c1.oid=c2.oid AND c1.oid < 10 ); - QUERY PLAN ---------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------- Nested Loop Semi Join Join Filter: (am.amname = c2.relname) -> Seq Scan on pg_am am -> Materialize -> Index Scan using pg_class_oid_index on pg_class c2 - Index Cond: ((oid < '10'::oid) AND (oid IS NOT NULL)) + Index Cond: (oid < '10'::oid) (6 rows) -- @@ -6619,14 +6619,14 @@ SELECT COUNT(*) FROM tab_with_flag WHERE (is_flag IS NULL OR is_flag = 0) AND id IN (SELECT id FROM tab_with_flag WHERE id IN (2, 3)); - QUERY PLAN ----------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------- Aggregate -> Bitmap Heap Scan on tab_with_flag - Recheck Cond: ((id = ANY ('{2,3}'::integer[])) AND (id IS NOT NULL)) + Recheck Cond: (id = ANY ('{2,3}'::integer[])) Filter: ((is_flag IS NULL) OR (is_flag = 0)) -> Bitmap Index Scan on tab_with_flag_pkey - Index Cond: ((id = ANY ('{2,3}'::integer[])) AND (id IS NOT NULL)) + Index Cond: (id = ANY ('{2,3}'::integer[])) (6 rows) DROP TABLE tab_with_flag; @@ -6745,11 +6745,11 @@ reset enable_seqscan; CREATE TABLE emp1 ( id SERIAL PRIMARY KEY NOT NULL, code int); explain (verbose, costs off) SELECT * FROM emp1 e1, emp1 e2 WHERE e1.id = e2.id AND e2.code <> e1.code; - QUERY PLAN ----------------------------------------------------------- + QUERY PLAN +------------------------------------------ Seq Scan on public.emp1 e2 Output: e2.id, e2.code, e2.id, e2.code - Filter: ((e2.id IS NOT NULL) AND (e2.code <> e2.code)) + Filter: (e2.code <> e2.code) (3 rows) -- Shuffle self-joined relations. Only in the case of iterative deletion @@ -6758,31 +6758,31 @@ CREATE UNIQUE INDEX ON emp1((id*id)); explain (costs off) SELECT count(*) FROM emp1 c1, emp1 c2, emp1 c3 WHERE c1.id=c2.id AND c1.id*c2.id=c3.id*c3.id; - QUERY PLAN ----------------------------------------------------------------- + QUERY PLAN +----------------------------------------- Aggregate -> Seq Scan on emp1 c3 - Filter: ((id IS NOT NULL) AND ((id * id) IS NOT NULL)) + Filter: ((id * id) IS NOT NULL) (3 rows) explain (costs off) SELECT count(*) FROM emp1 c1, emp1 c2, emp1 c3 WHERE c1.id=c3.id AND c1.id*c3.id=c2.id*c2.id; - QUERY PLAN ----------------------------------------------------------------- + QUERY PLAN +----------------------------------------- Aggregate -> Seq Scan on emp1 c3 - Filter: ((id IS NOT NULL) AND ((id * id) IS NOT NULL)) + Filter: ((id * id) IS NOT NULL) (3 rows) explain (costs off) SELECT count(*) FROM emp1 c1, emp1 c2, emp1 c3 WHERE c3.id=c2.id AND c3.id*c2.id=c1.id*c1.id; - QUERY PLAN ----------------------------------------------------------------- + QUERY PLAN +----------------------------------------- Aggregate -> Seq Scan on emp1 c3 - Filter: ((id IS NOT NULL) AND ((id * id) IS NOT NULL)) + Filter: ((id * id) IS NOT NULL) (3 rows) -- Check the usage of a parse tree by the set operations (bug #18170) @@ -6791,16 +6791,15 @@ SELECT c1.code FROM emp1 c1 LEFT JOIN emp1 c2 ON c1.id = c2.id WHERE c2.id IS NOT NULL EXCEPT ALL SELECT c3.code FROM emp1 c3; - QUERY PLAN ----------------------------------------------- + QUERY PLAN +------------------------------------------- HashSetOp Except All -> Append -> Subquery Scan on "*SELECT* 1" -> Seq Scan on emp1 c2 - Filter: (id IS NOT NULL) -> Subquery Scan on "*SELECT* 2" -> Seq Scan on emp1 c3 -(7 rows) +(6 rows) -- Check that SJE removes references from PHVs correctly explain (costs off) @@ -6809,8 +6808,8 @@ select * from emp1 t1 left join left join (emp1 t3 join emp1 t4 on t3.id = t4.id) on true) on true; - QUERY PLAN ----------------------------------------------------- + QUERY PLAN +--------------------------------------------- Nested Loop Left Join -> Seq Scan on emp1 t1 -> Materialize @@ -6818,8 +6817,7 @@ on true; -> Seq Scan on emp1 t2 -> Materialize -> Seq Scan on emp1 t4 - Filter: (id IS NOT NULL) -(8 rows) +(7 rows) -- Check that SJE does not remove self joins if a PHV references the removed -- rel laterally. diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out new file mode 100644 index 0000000000..47c8c1d2bd --- /dev/null +++ b/src/test/regress/expected/predicate.out @@ -0,0 +1,204 @@ +-- +-- Tests for predicate handling +-- +-- +-- test that restrictions that are always true are ignored, and that are always +-- false are replaced with constant-FALSE +-- +-- currently we only check for NullTest quals and OR clauses that include +-- NullTest quals. We may extend it in the future. +-- +create table pred_tab (a int not null, b int, c int not null); +-- 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) + +-- On the contrary, an IS_NOT_NULL qual in restriction clauses can not be +-- ignored if it's not on a NOT NULL column +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) + +-- An IS_NULL qual in restriction clauses can be reduced to constant-FALSE if +-- it's on a NOT NULL column +explain (costs off) +select * from pred_tab t where t.a is null; + QUERY PLAN +-------------------------- + Result + One-Time Filter: false +(2 rows) + +-- On the contrary, an IS_NULL qual in restriction clauses can not be reduced +-- to constant-FALSE if it's not on a NOT NULL column +explain (costs off) +select * from pred_tab t where t.b is null; + QUERY PLAN +------------------------ + Seq Scan on pred_tab t + Filter: (b IS NULL) +(2 rows) + +-- Tests for OR clauses in restriction clauses +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) + +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) + +explain (costs off) +select * from pred_tab t where t.a is null or t.c is null; + QUERY PLAN +-------------------------- + Result + One-Time Filter: false +(2 rows) + +explain (costs off) +select * from pred_tab t where t.b is null or t.c is null; + QUERY PLAN +---------------------------------------- + Seq Scan on pred_tab t + Filter: ((b IS NULL) OR (c IS NULL)) +(2 rows) + +-- An IS_NOT_NULL qual 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 +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) + +-- Otherwise the IS_NOT_NULL qual in join clauses cannot be ignored +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) + +-- An IS_NULL qual in join clauses can be reduced to constant-FALSE if +-- a) it's on a NOT NULL column, and +-- b) its Var is not nulled by any outer joins +explain (costs off) +select * from pred_tab t1 left join pred_tab t2 on true left join pred_tab t3 on t2.a is null and t2.b = 1; + QUERY PLAN +--------------------------------------------------- + Nested Loop Left Join + -> Seq Scan on pred_tab t1 + -> Materialize + -> Nested Loop Left Join + Join Filter: (false AND (t2.b = 1)) + -> Seq Scan on pred_tab t2 + -> Result + One-Time Filter: false +(8 rows) + +-- Otherwise the IS_NULL qual in join clauses cannot be reduced to constant-FALSE +explain (costs off) +select * from pred_tab t1 left join pred_tab t2 on true left join pred_tab t3 on t2.a is null; + QUERY PLAN +------------------------------------------- + Nested Loop Left Join + Join Filter: (t2.a IS NULL) + -> Nested Loop Left Join + -> Seq Scan on pred_tab t1 + -> Materialize + -> Seq Scan on pred_tab t2 + -> Materialize + -> Seq Scan on pred_tab t3 +(8 rows) + +-- Tests for OR clauses in join clauses +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) + +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) + +explain (costs off) +select * from pred_tab t1 left join pred_tab t2 on true left join pred_tab t3 on (t2.a is null or t2.c is null) and t2.b = 1; + QUERY PLAN +--------------------------------------------------- + Nested Loop Left Join + -> Seq Scan on pred_tab t1 + -> Materialize + -> Nested Loop Left Join + Join Filter: (false AND (t2.b = 1)) + -> Seq Scan on pred_tab t2 + -> Result + One-Time Filter: false +(8 rows) + +explain (costs off) +select * from pred_tab t1 left join pred_tab t2 on true left join pred_tab t3 on t2.a is null or t2.c is null; + QUERY PLAN +--------------------------------------------------- + Nested Loop Left Join + Join Filter: ((t2.a IS NULL) OR (t2.c IS NULL)) + -> Nested Loop Left Join + -> Seq Scan on pred_tab t1 + -> Materialize + -> Seq Scan on pred_tab t2 + -> Materialize + -> Seq Scan on pred_tab t3 +(8 rows) + +drop table pred_tab; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index f0987ff537..6f5a33c234 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..9d4336861f --- /dev/null +++ b/src/test/regress/sql/predicate.sql @@ -0,0 +1,80 @@ +-- +-- Tests for predicate handling +-- + +-- +-- test that restrictions that are always true are ignored, and that are always +-- false are replaced with constant-FALSE +-- +-- currently we only check for NullTest quals and OR clauses that include +-- NullTest quals. We may extend it in the future. +-- +create table pred_tab (a int not null, b int, c int not null); + +-- 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; + +-- On the contrary, an IS_NOT_NULL qual in restriction clauses can not be +-- ignored if it's not on a NOT NULL column +explain (costs off) +select * from pred_tab t where t.b is not null; + +-- An IS_NULL qual in restriction clauses can be reduced to constant-FALSE if +-- it's on a NOT NULL column +explain (costs off) +select * from pred_tab t where t.a is null; + +-- On the contrary, an IS_NULL qual in restriction clauses can not be reduced +-- to constant-FALSE if it's not on a NOT NULL column +explain (costs off) +select * from pred_tab t where t.b is null; + +-- Tests for OR clauses in restriction clauses +explain (costs off) +select * from pred_tab t where t.a is not null or t.b = 1; + +explain (costs off) +select * from pred_tab t where t.b is not null or t.a = 1; + +explain (costs off) +select * from pred_tab t where t.a is null or t.c is null; + +explain (costs off) +select * from pred_tab t where t.b is null or t.c is null; + +-- An IS_NOT_NULL qual 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 +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; + +-- Otherwise the IS_NOT_NULL qual in join clauses cannot be ignored +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; + +-- An IS_NULL qual in join clauses can be reduced to constant-FALSE if +-- a) it's on a NOT NULL column, and +-- b) its Var is not nulled by any outer joins +explain (costs off) +select * from pred_tab t1 left join pred_tab t2 on true left join pred_tab t3 on t2.a is null and t2.b = 1; + +-- Otherwise the IS_NULL qual in join clauses cannot be reduced to constant-FALSE +explain (costs off) +select * from pred_tab t1 left join pred_tab t2 on true left join pred_tab t3 on t2.a is null; + +-- Tests for OR clauses in join clauses +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; + +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; + +explain (costs off) +select * from pred_tab t1 left join pred_tab t2 on true left join pred_tab t3 on (t2.a is null or t2.c is null) and t2.b = 1; + +explain (costs off) +select * from pred_tab t1 left join pred_tab t2 on true left join pred_tab t3 on t2.a is null or t2.c is null; + +drop table pred_tab; -- 2.40.1.windows.1