On Wed, 10 Apr 2024 at 19:12, Richard Guo <guofengli...@gmail.com> wrote: > And I think recording NOT NULL columns for traditional inheritance > parents can be error-prone for some future optimization where we look > at an inheritance parent's notnullattnums and make decisions based on > the assumption that the included columns are non-nullable. The issue > discussed here serves as an example of this potential problem.
I admit that it seems more likely that having a member set in notnullattnums for an inheritance parent is more likely to cause future bugs than if we just leave them blank. But I also don't believe leaving them all blank is the right thing unless we document that the field isn't populated for traditional inheritance parent tables and if the code needs to not the NOT NULL status of a column for that table ONLY, then the code should look at the RelOptInfo corresponding to the inh==false RangeTblEntry for that relation. If we don't document the fact that we don't set the notnullattnums field then someone might write some code thinking we correctly populate it. If the parent and all children have NOT NULL constraints for a column, then unless we document we don't populate notnullattnums, it seems reasonable to assume that's a bug. If we skip populating notnullattnums for inh==true non-partitioned tables, I think we also still need to skip applying the NOT NULL qual optimisation for inh==true RTEs as my version of the code did. Reasons being: 1) it's a pointless exercise since we'll always end up adding the RestrictInfo without modification to the RelOptInfo's baserestrictinfo, and 2) The optimisation in question would be looking at the notnullattnums that isn't correctly populated. Resulting patch attached. David
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index d3868b628d..8981031d5d 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -2629,32 +2629,46 @@ add_base_clause_to_rel(PlannerInfo *root, Index relid, RestrictInfo *restrictinfo) { RelOptInfo *rel = find_base_rel(root, relid); + RangeTblEntry *rte = root->simple_rte_array[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 the origin qual with constant-FALSE if it is provably always - * false. Note that we keep the same rinfo_serial. + * For inheritance parent tables, we must always record the + * RestrictInfo in baserestrictinfo as is. If we were to transform or + * skip adding it, then the original wouldn't be available in + * apply_child_basequals. Since there are two RangeTblEntries for + * inheritance parents, one with inh==true and the other with inh==false, + * we're still able to apply this optimization to the inh==false one. The + * inh==true one is what apply_child_basequals() sees, whereas the + * inh==false one is what's used for the scan node in the final plan. */ - if (restriction_is_always_false(root, restrictinfo)) + if (!rte->inh) { - 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; + /* Don't add the clause if it is always true */ + if (restriction_is_always_true(root, restrictinfo)) + return; + + /* + * Substitute the origin qual with constant-FALSE if it is provably always + * false. Note that we keep the same rinfo_serial. + */ + 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 */ diff --git a/src/backend/optimizer/util/inherit.c b/src/backend/optimizer/util/inherit.c index 5c7acf8a90..761ad101b3 100644 --- a/src/backend/optimizer/util/inherit.c +++ b/src/backend/optimizer/util/inherit.c @@ -875,6 +875,7 @@ apply_child_basequals(PlannerInfo *root, RelOptInfo *parentrel, { Node *onecq = (Node *) lfirst(lc2); bool pseudoconstant; + RestrictInfo *childrinfo; /* check for pseudoconstant (no Vars or volatile functions) */ pseudoconstant = @@ -886,15 +887,23 @@ apply_child_basequals(PlannerInfo *root, RelOptInfo *parentrel, root->hasPseudoConstantQuals = true; } /* reconstitute RestrictInfo with appropriate properties */ - childquals = lappend(childquals, - make_restrictinfo(root, - (Expr *) onecq, - rinfo->is_pushed_down, - rinfo->has_clone, - rinfo->is_clone, - pseudoconstant, - rinfo->security_level, - NULL, NULL, NULL)); + childrinfo = make_restrictinfo(root, + (Expr *) onecq, + rinfo->is_pushed_down, + rinfo->has_clone, + rinfo->is_clone, + pseudoconstant, + rinfo->security_level, + NULL, NULL, NULL); + + /* Restriction is proven always false */ + if (restriction_is_always_false(root, childrinfo)) + return false; + /* Restriction is proven always true, so drop it */ + if (restriction_is_always_true(root, childrinfo)) + continue; + + childquals = lappend(childquals, childrinfo); /* track minimum security level among child quals */ cq_min_security = Min(cq_min_security, rinfo->security_level); } diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index 6bb53e4346..130f838629 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -161,22 +161,33 @@ 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++) + /* + * Record which columns are defined as NOT NULL. We leave this + * unpopulated for non-partitioned inheritance parent relations as it's + * ambiguous as to what it means. Some child tables may have a NOT NULL + * constraint for a column while others may not. We could work harder and + * build a unioned set of all child relations notnullattnums, but there's + * currently no need. The RelOptInfo corresponding to the !inh + * RangeTblEntry does get populated. + */ + if (!inhparent || relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) { - FormData_pg_attribute *attr = &relation->rd_att->attrs[i]; - - if (attr->attnotnull) + for (int i = 0; i < relation->rd_att->natts; i++) { - rel->notnullattnums = bms_add_member(rel->notnullattnums, - attr->attnum); + FormData_pg_attribute *attr = &relation->rd_att->attrs[i]; - /* - * Per RemoveAttributeById(), dropped columns will have their - * attnotnull unset, so we needn't check for dropped columns in - * the above condition. - */ - Assert(!attr->attisdropped); + if (attr->attnotnull) + { + rel->notnullattnums = bms_add_member(rel->notnullattnums, + attr->attnum); + + /* + * Per RemoveAttributeById(), dropped columns will have their + * attnotnull unset, so we needn't check for dropped columns + * in the above condition. + */ + Assert(!attr->attisdropped); + } } } diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index d791c4108d..2bbb9aabb6 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -372,6 +372,13 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent) break; } + /* + * We must apply the partially filled in RelOptInfo before calling + * apply_child_basequals due to some transformations within that function + * which require the RelOptInfo to be available in the simple_rel_array. + */ + root->simple_rel_array[relid] = rel; + /* * Copy the parent's quals to the child, with appropriate substitution of * variables. If any constant false or NULL clauses turn up, we can mark @@ -393,9 +400,6 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent) } } - /* Save the finished struct in the query's simple_rel_array */ - root->simple_rel_array[relid] = rel; - return rel; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 0ab25d9ce7..6c71098f2d 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -918,7 +918,11 @@ 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 */ + + /* + * Zero-based set containing attnums of NOT NULL columns. Not populated + * for rels corresponding to non-partitioned inh==true RTEs. + */ Bitmapset *notnullattnums; /* relids of outer joins that can null this baserel */ Relids nulling_relids; diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out index 60bf3e37aa..57511718d1 100644 --- a/src/test/regress/expected/predicate.out +++ b/src/test/regress/expected/predicate.out @@ -242,3 +242,51 @@ SELECT * FROM pred_tab t1 (9 rows) DROP TABLE pred_tab; +-- Validate we handle IS NULL and IS NOT NULL quals correctly with inheritance +-- parents. +CREATE TABLE pred_parent (a int, b int); +CREATE TABLE pred_child () INHERITS (pred_parent); +ALTER TABLE ONLY pred_parent ALTER a SET NOT NULL; +-- Ensure that the scan on pred_child contains the IS NOT NULL qual. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_parent WHERE a IS NOT NULL; + QUERY PLAN +--------------------------------------------- + Append + -> Seq Scan on pred_parent pred_parent_1 + -> Seq Scan on pred_child pred_parent_2 + Filter: (a IS NOT NULL) +(4 rows) + +-- Ensure we only scan pred_child and not pred_parent +EXPLAIN (COSTS OFF) +SELECT * FROM pred_parent WHERE a IS NULL; + QUERY PLAN +------------------------------------ + Seq Scan on pred_child pred_parent + Filter: (a IS NULL) +(2 rows) + +ALTER TABLE pred_parent ALTER a DROP NOT NULL; +ALTER TABLE pred_child ALTER a SET NOT NULL; +-- Ensure the IS NOT NULL qual is removed from the pred_child scan. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_parent WHERE a IS NOT NULL; + QUERY PLAN +--------------------------------------------- + Append + -> Seq Scan on pred_parent pred_parent_1 + Filter: (a IS NOT NULL) + -> Seq Scan on pred_child pred_parent_2 +(4 rows) + +-- Ensure we only scan pred_parent and not pred_child +EXPLAIN (COSTS OFF) +SELECT * FROM pred_parent WHERE a IS NULL; + QUERY PLAN +------------------------- + Seq Scan on pred_parent + Filter: (a IS NULL) +(2 rows) + +DROP TABLE pred_parent, pred_child; diff --git a/src/test/regress/sql/predicate.sql b/src/test/regress/sql/predicate.sql index 563e395fde..a09ec38828 100644 --- a/src/test/regress/sql/predicate.sql +++ b/src/test/regress/sql/predicate.sql @@ -120,3 +120,30 @@ SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t3 ON t2.a IS NULL OR t2.c IS NULL; DROP TABLE pred_tab; + +-- Validate we handle IS NULL and IS NOT NULL quals correctly with inheritance +-- parents. +CREATE TABLE pred_parent (a int, b int); +CREATE TABLE pred_child () INHERITS (pred_parent); +ALTER TABLE ONLY pred_parent ALTER a SET NOT NULL; + +-- Ensure that the scan on pred_child contains the IS NOT NULL qual. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_parent WHERE a IS NOT NULL; + +-- Ensure we only scan pred_child and not pred_parent +EXPLAIN (COSTS OFF) +SELECT * FROM pred_parent WHERE a IS NULL; + +ALTER TABLE pred_parent ALTER a DROP NOT NULL; +ALTER TABLE pred_child ALTER a SET NOT NULL; + +-- Ensure the IS NOT NULL qual is removed from the pred_child scan. +EXPLAIN (COSTS OFF) +SELECT * FROM pred_parent WHERE a IS NOT NULL; + +-- Ensure we only scan pred_parent and not pred_child +EXPLAIN (COSTS OFF) +SELECT * FROM pred_parent WHERE a IS NULL; + +DROP TABLE pred_parent, pred_child;