@@ -213,16 +213,19 @@ typedef struct AlteredTableInfo
213213 List *changedStatisticsDefs; /* string definitions of same */
214214} AlteredTableInfo;
215215
216- /* Struct describing one new constraint to check in Phase 3 scan */
217- /* Note: new not-null constraints are handled elsewhere */
216+ /*
217+ * Struct describing one new constraint to check in Phase 3 scan. Note: new
218+ * not-null constraints were also added to Phase3.
219+ */
218220typedef struct NewConstraint
219221{
220222 char *name; /* Constraint name, or NULL if none */
221- ConstrType contype; /* CHECK or FOREIGN */
223+ ConstrType contype; /* CHECK or FOREIGN or NOT NULL */
222224 Oid refrelid; /* PK rel, if FOREIGN */
223225 Oid refindid; /* OID of PK's index, if FOREIGN */
224226 bool conwithperiod; /* Whether the new FOREIGN KEY uses PERIOD */
225227 Oid conid; /* OID of pg_constraint entry, if FOREIGN */
228+ int attnum; /* NOT NULL constraint attribute number */
226229 Node *qual; /* Check expr or CONSTR_FOREIGN Constraint */
227230 ExprState *qualstate; /* Execution state for CHECK expr */
228231} NewConstraint;
@@ -794,6 +797,7 @@ static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab,
794797static List *collectPartitionIndexExtDeps(List *partitionOids);
795798static void applyPartitionIndexExtDeps(Oid newPartOid, List *extDepState);
796799static void freePartitionIndexExtDeps(List *extDepState);
800+ static bool index_check_notnull(Relation relation, List *notnull_attnums);
797801
798802/* ----------------------------------------------------------------
799803 * DefineRelation
@@ -6312,6 +6316,9 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
63126316 needscan = true;
63136317 con->qualstate = ExecPrepareExpr((Expr *) expand_generated_columns_in_expr(con->qual, oldrel, 1), estate);
63146318 break;
6319+ case CONSTR_NOTNULL:
6320+ /* Nothing to do here */
6321+ break;
63156322 case CONSTR_FOREIGN:
63166323 /* Nothing to do here */
63176324 break;
@@ -6339,6 +6346,78 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
63396346 notnull_attrs = notnull_virtual_attrs = NIL;
63406347 if (newrel || tab->verify_new_notnull)
63416348 {
6349+ bool nullsChecked = false;
6350+
6351+ /*
6352+ * The conditions for using indexscan mechanism fast verifying
6353+ * not-null constraints are quite strict. All of the following
6354+ * conditions must be met.
6355+ *
6356+ * 1. AlteredTableInfo->verify_new_notnull is true.
6357+ *
6358+ * 2. No table scan (e.g., for CHECK constraint verification) or table
6359+ * rewrite is expected later, if one is, using indexscan would just
6360+ * wastes cycles.
6361+ *
6362+ * 3. Indexes cannot be created on virtual generated columns, so fast
6363+ * checking not-null constraints is not applicable to them.
6364+ *
6365+ * 4. The relation must be a plain table.
6366+ *
6367+ * 5. To prevent possible concurrency issues, the table must already
6368+ * be locked with an AccessExclusiveLock, which is the lock obtained
6369+ * during ALTER TABLE SET NOT NULL.
6370+ */
6371+ if (!needscan &&
6372+ newrel == NULL &&
6373+ oldrel->rd_rel->relkind == RELKIND_RELATION &&
6374+ CheckRelationLockedByMe(oldrel, AccessExclusiveLock, false))
6375+ {
6376+ List *notnull_attnums = NIL;
6377+
6378+ Assert(!tab->rewrite);
6379+
6380+ foreach(l, tab->constraints)
6381+ {
6382+ Form_pg_attribute attr;
6383+ NewConstraint *con = lfirst(l);
6384+
6385+ if (con->contype != CONSTR_NOTNULL)
6386+ continue;
6387+
6388+ attr = TupleDescAttr(newTupDesc, con->attnum - 1);
6389+
6390+ if (attr->attisdropped)
6391+ continue;
6392+
6393+ Assert(attr->attnotnull);
6394+ Assert(attr->attnum == con->attnum);
6395+
6396+ if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
6397+ {
6398+ needscan = true;
6399+ break;
6400+ }
6401+
6402+ notnull_attnums = list_append_unique_int(notnull_attnums,
6403+ attr->attnum);
6404+ }
6405+
6406+ if (!needscan)
6407+ {
6408+ if (index_check_notnull(oldrel, notnull_attnums))
6409+ {
6410+ nullsChecked = true;
6411+
6412+ ereport(DEBUG1,
6413+ errmsg_internal("all new not-null constraints on relation \"%s\" have been validated by using index scan",
6414+ RelationGetRelationName(oldrel)));
6415+ }
6416+ else
6417+ needscan = true;
6418+ }
6419+ }
6420+
63426421 /*
63436422 * If we are rebuilding the tuples OR if we added any new but not
63446423 * verified not-null constraints, check all *valid* not-null
@@ -6349,7 +6428,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
63496428 * not-null constraints over virtual generated columns; instead, they
63506429 * are collected in notnull_virtual_attrs for verification elsewhere.
63516430 */
6352- for (i = 0; i < newTupDesc->natts; i++)
6431+ for (i = 0; !nullsChecked && i < newTupDesc->natts; i++)
63536432 {
63546433 CompactAttribute *attr = TupleDescCompactAttr(newTupDesc, i);
63556434
@@ -6367,6 +6446,9 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
63676446 }
63686447 if (notnull_attrs || notnull_virtual_attrs)
63696448 needscan = true;
6449+
6450+ if (nullsChecked)
6451+ Assert(needscan == false);
63706452 }
63716453
63726454 if (newrel || needscan)
@@ -8057,6 +8139,7 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
80578139 CookedConstraint *ccon;
80588140 List *cooked;
80598141 bool is_no_inherit = false;
8142+ AlteredTableInfo *tab = ATGetQueueEntry(wqueue, rel);
80608143
80618144 /* Guard against stack overflow due to overly deep inheritance tree. */
80628145 check_stack_depth();
@@ -8185,6 +8268,25 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
81858268 cooked = AddRelationNewConstraints(rel, NIL, list_make1(constraint),
81868269 false, !recursing, false, NULL);
81878270 ccon = linitial(cooked);
8271+
8272+ Assert(ccon->contype == CONSTR_NOTNULL);
8273+
8274+ /*
8275+ * Add this to-be-validated not-null constraint to Phase 3's queue. We may
8276+ * able to use indexscan mechanism to verify this not-null constraint in
8277+ * Phase3.
8278+ */
8279+ if (!ccon->skip_validation)
8280+ {
8281+ NewConstraint *newcon = palloc0_object(NewConstraint);
8282+
8283+ newcon->name = ccon->name;
8284+ newcon->contype = CONSTR_NOTNULL;
8285+ newcon->attnum = ccon->attnum;
8286+
8287+ tab->constraints = lappend(tab->constraints, newcon);
8288+ }
8289+
81888290 ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
81898291
81908292 /* Mark pg_attribute.attnotnull for the column and queue validation */
@@ -10080,13 +10182,15 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
1008010182 {
1008110183 CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon);
1008210184
10083- if (!ccon->skip_validation && ccon->contype != CONSTR_NOTNULL )
10185+ if (!ccon->skip_validation)
1008410186 {
1008510187 NewConstraint *newcon;
1008610188
1008710189 newcon = palloc0_object(NewConstraint);
1008810190 newcon->name = ccon->name;
1008910191 newcon->contype = ccon->contype;
10192+ if (ccon->contype == CONSTR_NOTNULL)
10193+ newcon->attnum = ccon->attnum;
1009010194 newcon->qual = ccon->expr;
1009110195
1009210196 tab->constraints = lappend(tab->constraints, newcon);
@@ -13755,6 +13859,7 @@ QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
1375513859 List *children = NIL;
1375613860 AttrNumber attnum;
1375713861 char *colname;
13862+ NewConstraint *newcon;
1375813863
1375913864 con = (Form_pg_constraint) GETSTRUCT(contuple);
1376013865 Assert(con->contype == CONSTRAINT_NOTNULL);
@@ -13819,6 +13924,20 @@ QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
1381913924 set_attnotnull(NULL, rel, attnum, false);
1382013925
1382113926 tab = ATGetQueueEntry(wqueue, rel);
13927+
13928+ /*
13929+ * Queue validation for phase 3. ALTER TABLE SET NOT NULL adds a NOT NULL
13930+ * constraint (NewConstraint) to AlteredTableInfo->constraints; for
13931+ * consistency, we do the same here. This setup allows phase 3
13932+ * (ATRewriteTable) to quickly validate the column's NULL status using an
13933+ * index scan, if all conditions are met.
13934+ */
13935+ newcon = palloc0_object(NewConstraint);
13936+ newcon->name = colname;
13937+ newcon->contype = CONSTR_NOTNULL;
13938+ newcon->attnum = attnum;
13939+ tab->constraints = lappend(tab->constraints, newcon);
13940+
1382213941 tab->verify_new_notnull = true;
1382313942
1382413943 /*
@@ -24380,3 +24499,179 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
2438024499 /* Restore the userid and security context. */
2438124500 SetUserIdAndSecContext(save_userid, save_sec_context);
2438224501}
24502+
24503+ /*
24504+ * notnull_attnums: list of attribute numbers for all newly added NOT NULL
24505+ * constraints.
24506+ *
24507+ * For each NOT NULL attribute, first check whether the relation has a suitable
24508+ * index to fetch that attribute content. If so, using indexscan to verify that
24509+ * attribute contains no NULL values.
24510+ *
24511+ * Returning true means the new NOT NULL constraints are fully verified and no
24512+ * extra table scans are necessary.
24513+ */
24514+ static bool
24515+ index_check_notnull(Relation relation, List *notnull_attnums)
24516+ {
24517+ SysScanDesc indscan;
24518+ ScanKeyData skey;
24519+ List *idxs = NIL;
24520+ List *attnums = NIL;
24521+ bool all_not_null = true;
24522+ ListCell *lc,
24523+ *lc2;
24524+ Relation pg_index;
24525+ Relation indexRel;
24526+ HeapTuple indexTuple;
24527+
24528+ if (notnull_attnums == NIL)
24529+ return false;
24530+
24531+ pg_index = table_open(IndexRelationId, AccessShareLock);
24532+
24533+ /* Prepare to scan pg_index for entries having indrelid = this rel */
24534+ ScanKeyInit(&skey,
24535+ Anum_pg_index_indrelid,
24536+ BTEqualStrategyNumber,
24537+ F_OIDEQ,
24538+ ObjectIdGetDatum(RelationGetRelid(relation)));
24539+
24540+ indscan = systable_beginscan(pg_index, IndexIndrelidIndexId, true,
24541+ NULL, 1, &skey);
24542+
24543+ while (HeapTupleIsValid(indexTuple = systable_getnext(indscan)))
24544+ {
24545+ Form_pg_attribute attr;
24546+
24547+ Form_pg_index index = (Form_pg_index) GETSTRUCT(indexTuple);
24548+
24549+ /*
24550+ * We only use non-deferred, valid, and live b-tree indexes to verify
24551+ * NOT NULL constraints.
24552+ */
24553+ if (!index->indimmediate || !index->indisvalid || !index->indislive)
24554+ continue;
24555+
24556+ /* cannot use expression index or partial index too */
24557+ if (!heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL) ||
24558+ !heap_attisnull(indexTuple, Anum_pg_index_indpred, NULL))
24559+ continue;
24560+
24561+ indexRel = index_open(index->indexrelid, AccessShareLock);
24562+
24563+ if (indexRel->rd_rel->relam != BTREE_AM_OID)
24564+ {
24565+ index_close(indexRel, NoLock);
24566+
24567+ continue;
24568+ }
24569+
24570+ /*
24571+ * If the index is valid, but cannot yet be used, ignore it; See
24572+ * src/backend/access/heap/README.HOT for discussion.
24573+ */
24574+ if (index->indcheckxmin &&
24575+ !TransactionIdPrecedes(HeapTupleHeaderGetXmin(indexRel->rd_indextuple->t_data),
24576+ TransactionXmin))
24577+ {
24578+ index_close(indexRel, NoLock);
24579+ continue;
24580+ }
24581+
24582+ attr = TupleDescAttr(RelationGetDescr(relation), (index->indkey.values[0] - 1));
24583+
24584+ if (list_member_int(notnull_attnums, attr->attnum) &&
24585+ !list_member_int(attnums, attr->attnum))
24586+ {
24587+ attnums = lappend_int(attnums, attr->attnum);
24588+ idxs = lappend_oid(idxs, index->indexrelid);
24589+ }
24590+
24591+ index_close(indexRel, NoLock);
24592+ }
24593+ systable_endscan(indscan);
24594+ table_close(pg_index, NoLock);
24595+
24596+ /*
24597+ * Verify NOT NULL constraints using a suitable index, falling back to a
24598+ * full table scan if a suitable index is not present.
24599+ */
24600+ if (attnums == NIL ||
24601+ list_length(notnull_attnums) != list_length(attnums))
24602+ return false;
24603+
24604+ foreach_int(attno, notnull_attnums)
24605+ {
24606+ if (!list_member_int(attnums, attno))
24607+ return false;
24608+ }
24609+
24610+ forboth(lc, attnums, lc2, idxs)
24611+ {
24612+ SnapshotData DirtySnapshot;
24613+ IndexScanDesc indexScan;
24614+ ScanKeyData scankeys[INDEX_MAX_KEYS];
24615+ AttrNumber sk_attno = -1;
24616+ AttrNumber attno = lfirst_int(lc);
24617+ Oid indexoid = lfirst_oid(lc2);
24618+ IndexInfo *indexInfo;
24619+ TupleTableSlot *existing_slot;
24620+
24621+ indexRel = index_open(indexoid, NoLock);
24622+ indexInfo = BuildIndexInfo(indexRel);
24623+ existing_slot = table_slot_create(relation, NULL);
24624+
24625+ /*
24626+ * Search the tuples that are in the index for any violations,
24627+ * including tuples that aren't visible yet.
24628+ */
24629+ InitDirtySnapshot(DirtySnapshot);
24630+
24631+ if (indexInfo->ii_IndexAttrNumbers[0] == attno)
24632+ sk_attno = 1;
24633+ else
24634+ elog(ERROR, "cache lookup failed for attribute number %d on index %u",
24635+ indexoid, attno);
24636+
24637+ /* set up an IS NULL scan key so that we ignore not nulls */
24638+ ScanKeyEntryInitialize(&scankeys[0],
24639+ SK_ISNULL | SK_SEARCHNULL,
24640+ sk_attno, /* index col to scan */
24641+ InvalidStrategy, /* no strategy */
24642+ InvalidOid, /* no strategy subtype */
24643+ InvalidOid, /* no collation */
24644+ InvalidOid, /* no reg proc for this */
24645+ (Datum) 0); /* constant */
24646+
24647+ indexScan = index_beginscan(relation,
24648+ indexRel,
24649+ &DirtySnapshot,
24650+ NULL,
24651+ 1,
24652+ 0,
24653+ SO_NONE);
24654+ index_rescan(indexScan, scankeys, 1, NULL, 0);
24655+
24656+ while (index_getnext_slot(indexScan, ForwardScanDirection, existing_slot))
24657+ {
24658+ /*
24659+ * At this point, we have found that some attribute value is NULL.
24660+ * Since btree indexes are never lossy, no need recheck the NULL
24661+ * condition.
24662+ */
24663+ all_not_null = false;
24664+ break;
24665+ }
24666+
24667+ index_endscan(indexScan);
24668+ index_close(indexRel, NoLock);
24669+ ExecDropSingleTupleTableSlot(existing_slot);
24670+
24671+ /* exit earlier */
24672+ if (!all_not_null)
24673+ return false;
24674+ }
24675+
24676+ return true;
24677+ }
0 commit comments