hi. v11 is attached. Mainly fixes regress test failures.
-- jian https://www.enterprisedb.com/
From 8b38632332089b0674ff5d40419c988a61ce23bb Mon Sep 17 00:00:00 2001 From: jian he <[email protected]> Date: Wed, 4 Feb 2026 20:50:26 +0800 Subject: [PATCH v11 1/1] using indexscan to speedup add not null constraints This patch tries to use index_beginscan() / index_getnext() / index_endscan() mentioned in [1] to speedup adding not-null constraints to the existing table. The main logic happens in phase3 ATRewriteTable 1. collect all not-null constraints. 2. For each not-null constraint, check whether there is a corresponding index available for validation. If not, then cannot use indexscan mechanism to verify not-null constraints. 3. If any of the following conditions are true * table scan or rewrite is required, * table wasn't locked with `AccessExclusiveLock` * the NOT NULL constraint applies to a virtual generated column then index scan cannot be used for fast validation. If all conditions are satisfied, attempt to use indexscan to verify whether the column contains any NULL values. concurrency concern: ALTER TABLE SET NOT NULL will take an ACCESS EXCLUSIVE lock, so there is less variant of racing issue can occur? to prove accurate, I wrote some isolation tests. see[2] references: [1] https://postgr.es/m/CA%2BTgmoa5NKz8iGW_9v7wz%3D-%2BzQFu%3DE4SZoaTaU1znLaEXRYp-Q%40mail.gmail.com [2] https://postgr.es/m/900056D1-32DF-4927-8251-3E0C0DC407FD%40anarazel.de discussion: https://postgr.es/m/CACJufxFiW=4k1is=F1J=r-cx1rubyxqpurwb331u47rsngz...@mail.gmail.com commitfest entry: https://commitfest.postgresql.org/patch/5444 --- src/backend/commands/tablecmds.c | 285 +++++++++++++++++++++- src/test/regress/expected/constraints.out | 65 +++++ src/test/regress/sql/constraints.sql | 47 ++++ 3 files changed, 391 insertions(+), 6 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index f976c0e5c7e..49dbc690e90 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -210,16 +210,19 @@ typedef struct AlteredTableInfo List *changedStatisticsDefs; /* string definitions of same */ } AlteredTableInfo; -/* Struct describing one new constraint to check in Phase 3 scan */ -/* Note: new not-null constraints are handled elsewhere */ +/* + * Struct describing one new constraint to check in Phase 3 scan. Note: new + * not-null constraints are handled here too. +*/ typedef struct NewConstraint { char *name; /* Constraint name, or NULL if none */ - ConstrType contype; /* CHECK or FOREIGN */ + ConstrType contype; /* CHECK or FOREIGN or NOT NULL */ Oid refrelid; /* PK rel, if FOREIGN */ Oid refindid; /* OID of PK's index, if FOREIGN */ bool conwithperiod; /* Whether the new FOREIGN KEY uses PERIOD */ Oid conid; /* OID of pg_constraint entry, if FOREIGN */ + int attnum; /* NOT NULL constraint attribute number */ Node *qual; /* Check expr or CONSTR_FOREIGN Constraint */ ExprState *qualstate; /* Execution state for CHECK expr */ } NewConstraint; @@ -745,6 +748,7 @@ static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, PartitionCmd *cmd, AlterTableUtilityContext *context); +static bool index_check_notnull(Relation relation, List *notnull_attnums); /* ---------------------------------------------------------------- * DefineRelation @@ -6217,6 +6221,9 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap) needscan = true; con->qualstate = ExecPrepareExpr((Expr *) expand_generated_columns_in_expr(con->qual, oldrel, 1), estate); break; + case CONSTR_NOTNULL: + /* Nothing to do here */ + break; case CONSTR_FOREIGN: /* Nothing to do here */ break; @@ -6270,10 +6277,78 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap) wholeatt->attnum); } } - if (notnull_attrs || notnull_virtual_attrs) - needscan = true; } + /* + * The conditions for using indexscan mechanism fast verifying not-null + * constraints are quite strict. All of the following conditions must be + * met. + * + * 1. AlteredTableInfo->verify_new_notnull is true. + * + * 2. If table scan (for other reason, like check constraint verification) + * or table rewrite is expected later, using indexscan is just wastes + * cycles. + * + * 3. Indexes cannot be created on virtual generated columns, so fast + * checking not-null constraints is not applicable to them. + * + * 4. This applies to plain relations only. + * + * 5. To prevent possible concurrency issues, the table must already be + * locked with an AccessExclusiveLock, which is the lock obtained during + * ALTER TABLE SET NOT NULL. + */ + if (!needscan && + newrel == NULL && + tab->verify_new_notnull && + notnull_virtual_attrs == NIL && + oldrel->rd_rel->relkind == RELKIND_RELATION && + CheckRelationLockedByMe(oldrel, AccessExclusiveLock, false)) + { + List *notnull_attnums = NIL; + + Assert(!tab->rewrite); + + foreach(l, tab->constraints) + { + Form_pg_attribute attr; + NewConstraint *con = lfirst(l); + + /* + * This may be a FOREIGN KEY (even if not a CHECK constraint), + * therefore we cannot just raise error for non-NOT-NULL + * constraint. + */ + if (con->contype != CONSTR_NOTNULL) + continue; + + attr = TupleDescAttr(newTupDesc, con->attnum - 1); + + if (attr->attisdropped) + continue; + + Assert(attr->attnotnull); + Assert(attr->attnum == con->attnum); + Assert(attr->attgenerated != ATTRIBUTE_GENERATED_VIRTUAL); + + notnull_attnums = list_append_unique_int(notnull_attnums, + attr->attnum); + } + + if (notnull_attnums != NIL) + { + if (index_check_notnull(oldrel, notnull_attnums)) + ereport(DEBUG1, + errmsg_internal("all new not-null constraints on relation \"%s\" have been validated by using index scan", + RelationGetRelationName(oldrel))); + else + needscan = true; + } + } + else if (notnull_attrs || notnull_virtual_attrs) + needscan = true; + if (newrel || needscan) { ExprContext *econtext; @@ -7949,6 +8024,7 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName, CookedConstraint *ccon; List *cooked; bool is_no_inherit = false; + AlteredTableInfo *tab = ATGetQueueEntry(wqueue, rel);; /* Guard against stack overflow due to overly deep inheritance tree. */ check_stack_depth(); @@ -8077,6 +8153,25 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName, cooked = AddRelationNewConstraints(rel, NIL, list_make1(constraint), false, !recursing, false, NULL); ccon = linitial(cooked); + + Assert(ccon->contype == CONSTR_NOTNULL); + + /* + * We may using indexscan mechanism to vertify this not-null constraint in + * Phase3. Add this to-be-validated not-null constraint to Phase 3's + * queue. + */ + if (!ccon->skip_validation) + { + NewConstraint *newcon = palloc0_object(NewConstraint); + + newcon->name = ccon->name; + newcon->contype = CONSTR_NOTNULL; + newcon->attnum = ccon->attnum; + + tab->constraints = lappend(tab->constraints, newcon); + } + ObjectAddressSet(address, ConstraintRelationId, ccon->conoid); InvokeObjectPostAlterHook(RelationRelationId, @@ -9983,13 +10078,15 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, { CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon); - if (!ccon->skip_validation && ccon->contype != CONSTR_NOTNULL) + if (!ccon->skip_validation) { NewConstraint *newcon; newcon = palloc0_object(NewConstraint); newcon->name = ccon->name; newcon->contype = ccon->contype; + if (ccon->contype == CONSTR_NOTNULL) + newcon->attnum = ccon->attnum; newcon->qual = ccon->expr; tab->constraints = lappend(tab->constraints, newcon); @@ -13258,6 +13355,7 @@ QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel, List *children = NIL; AttrNumber attnum; char *colname; + NewConstraint *newcon; con = (Form_pg_constraint) GETSTRUCT(contuple); Assert(con->contype == CONSTRAINT_NOTNULL); @@ -13321,7 +13419,19 @@ QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel, /* Set attnotnull appropriately without queueing another validation */ set_attnotnull(NULL, rel, attnum, true, false); + /* + * Queue validation for phase 3. ALTER TABLE SET NOT NULL adds a NOT NULL + * constraint to AlteredTableInfo->constraints, for consistency, do the + * same here. + */ + newcon = palloc0_object(NewConstraint); + newcon->name = colname; + newcon->contype = CONSTR_NOTNULL; + newcon->attnum = attnum; + + /* Find or create work queue entry for this table */ tab = ATGetQueueEntry(wqueue, rel); + tab->constraints = lappend(tab->constraints, newcon); tab->verify_new_notnull = true; /* @@ -23340,3 +23450,166 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, /* Restore the userid and security context. */ SetUserIdAndSecContext(save_userid, save_sec_context); } + +/* + * notnull_attnums: list of attribute numbers for all newly added NOT NULL + * constraints. + * + * For each NOT NULL attribute, first check whether the relation has a suitable + * index to fetch that attribute content. If so, using indexscan to verify that + * attribute contains no NULL values. + * + * Returning true indicates that all new NOT NULL constraints have been + * validated and that no additional table scan is required. + */ +static bool +index_check_notnull(Relation relation, List *notnull_attnums) +{ + SysScanDesc indscan; + ScanKeyData skey; + HeapTuple htup; + List *idxs = NIL; + List *attnums = NIL; + bool all_not_null = true; + ListCell *lc, + *lc2; + + Relation pg_index = table_open(IndexRelationId, AccessShareLock); + + /* Prepare to scan pg_index for entries having indrelid = this rel */ + ScanKeyInit(&skey, + Anum_pg_index_indrelid, + BTEqualStrategyNumber, + F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(relation))); + + indscan = systable_beginscan(pg_index, IndexIndrelidIndexId, true, + NULL, 1, &skey); + + while (HeapTupleIsValid(htup = systable_getnext(indscan))) + { + Form_pg_attribute attr; + Relation index_rel; + + Form_pg_index index = (Form_pg_index) GETSTRUCT(htup); + + /* + * We only use non-deferred, valid, and alive btree index to vertify + * not-null constraints + */ + if (!index->indimmediate || !index->indisvalid || !index->indislive) + continue; + + /* cannot use expression index or partial index too */ + if (!heap_attisnull(htup, Anum_pg_index_indexprs, NULL) || + !heap_attisnull(htup, Anum_pg_index_indpred, NULL)) + continue; + + index_rel = index_open(index->indexrelid, AccessShareLock); + + if (index_rel->rd_rel->relam != BTREE_AM_OID) + { + index_close(index_rel, AccessShareLock); + + continue; + } + + attr = TupleDescAttr(RelationGetDescr(relation), (index->indkey.values[0] - 1)); + + if (list_member_int(notnull_attnums, attr->attnum) && + !list_member_int(attnums, attr->attnum)) + { + attnums = lappend_int(attnums, attr->attnum); + idxs = lappend_oid(idxs, index->indexrelid); + } + + index_close(index_rel, NoLock); + } + systable_endscan(indscan); + table_close(pg_index, AccessShareLock); + + /* + * Each NOT NULL constraint requires a suitable index for verification. If + * no such index exists, fall back to a regular table scan to verify the + * constraints. + */ + if (attnums == NIL || + list_length(notnull_attnums) != list_length(attnums)) + return false; + + foreach_int(attno, notnull_attnums) + { + if (!list_member_int(attnums, attno)) + return false; + } + + forboth(lc, attnums, lc2, idxs) + { + SnapshotData DirtySnapshot; + IndexScanDesc index_scan; + ScanKeyData scankeys[INDEX_MAX_KEYS]; + AttrNumber sk_attno = -1; + AttrNumber attno = lfirst_int(lc); + Oid indexoid = lfirst_oid(lc2); + + Relation idxrel = index_open(indexoid, NoLock); + IndexInfo *indexInfo = BuildIndexInfo(idxrel); + int indnkeyatts = IndexRelationGetNumberOfKeyAttributes(idxrel); + + TupleTableSlot *existing_slot = table_slot_create(relation, NULL); + + /* + * Search the tuples that are in the index for any violations, + * including tuples that aren't visible yet. + */ + InitDirtySnapshot(DirtySnapshot); + + if (indexInfo->ii_IndexAttrNumbers[0] == attno) + sk_attno = 1; + else + elog(ERROR, "cache lookup failed for attribute number %d on index %u", + indexoid, attno); + + for (int i = 0; i < indnkeyatts; i++) + { + /* set up an IS NULL scan key so that we ignore not nulls */ + ScanKeyEntryInitialize(&scankeys[i], + SK_ISNULL | SK_SEARCHNULL, + sk_attno, /* index col to scan */ + InvalidStrategy, /* no strategy */ + InvalidOid, /* no strategy subtype */ + InvalidOid, /* no collation */ + InvalidOid, /* no reg proc for this */ + (Datum) 0); /* constant */ + } + + index_scan = index_beginscan(relation, + idxrel, + &DirtySnapshot, + NULL, + indnkeyatts, + 0); + index_rescan(index_scan, scankeys, indnkeyatts, NULL, 0); + + while (index_getnext_slot(index_scan, ForwardScanDirection, existing_slot)) + { + /* + * At this point, we have found that some attribute value is NULL. + * btree indexes are never lossy, we don't need recheck the + * condition. + */ + all_not_null = false; + break; + } + + index_endscan(index_scan); + index_close(idxrel, NoLock); + ExecDropSingleTupleTableSlot(existing_slot); + + /* exit earlier */ + if (!all_not_null) + return false; + } + + return true; +} diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out index ebc892a2a42..069eee39ade 100644 --- a/src/test/regress/expected/constraints.out +++ b/src/test/regress/expected/constraints.out @@ -1717,3 +1717,68 @@ COMMENT ON CONSTRAINT inv_ck ON DOMAIN constraint_comments_dom IS 'comment on in CREATE TABLE regress_notnull1 (a integer); CREATE TABLE regress_notnull2 () INHERITS (regress_notnull1); ALTER TABLE ONLY regress_notnull2 ALTER COLUMN a SET NOT NULL; +CREATE TABLE tp_notnull (a int, b int, c int, d int, f1 int default 10, f2 int default 11) PARTITION BY range(a); +CREATE TABLE tp_notnull_1 partition of tp_notnull for values FROM ( 1 ) to (10); +CREATE TABLE tp_notnull_2 partition of tp_notnull for values FROM ( 10 ) to (21); +CREATE INDEX tp_notnull_idx_b ON tp_notnull(b); +CREATE INDEX tp_notnull_idx_c ON tp_notnull(c); +CREATE INDEX tp_notnull_idx_d ON tp_notnull(d); +CREATE INDEX tp_notnull_idx_d_include ON tp_notnull(d) include(f1); +CREATE INDEX tp_notnull_idx_f2 ON tp_notnull USING hash (f2 int4_ops); +INSERT INTO tp_notnull SELECT g, g + 10, g + 15, g + random(min=>1::int, max=>10::int) FROM generate_series(8,19) g; +SET client_min_messages TO DEBUG1; +SET log_statement to NONE; +--debug message should contain "have been validated by using index scan" +ALTER TABLE tp_notnull ALTER COLUMN b SET NOT NULL; +DEBUG: all new not-null constraints on relation "tp_notnull_1" have been validated by using index scan +DEBUG: all new not-null constraints on relation "tp_notnull_2" have been validated by using index scan +ALTER TABLE tp_notnull ALTER COLUMN b DROP NOT NULL; +--debug message should contain "have been validated by using index scan" +ALTER TABLE tp_notnull_1 ALTER COLUMN b SET NOT NULL; +DEBUG: all new not-null constraints on relation "tp_notnull_1" have been validated by using index scan +--debug message should contain "have been validated by using index scan" +ALTER TABLE tp_notnull_2 ALTER COLUMN b SET NOT NULL; +DEBUG: all new not-null constraints on relation "tp_notnull_2" have been validated by using index scan +ALTER TABLE tp_notnull_2 ADD CONSTRAINT nn_d NOT NULL d NOT VALID; +--debug message should not contain "have been validated by using index scan" +ALTER TABLE tp_notnull_2 VALIDATE CONSTRAINT nn_d; +DEBUG: verifying table "tp_notnull_2" +--debug message should not contain "have been validated by using index scan" +ALTER TABLE tp_notnull ADD CONSTRAINT nnf1 NOT NULL f1; +DEBUG: verifying table "tp_notnull_1" +DEBUG: verifying table "tp_notnull_2" +--debug message should not contain "have been validated by using index scan" +ALTER TABLE tp_notnull ADD CONSTRAINT nnf2 NOT NULL f2; +DEBUG: verifying table "tp_notnull_1" +DEBUG: verifying table "tp_notnull_2" +--debug message should not contain "have been validated by using index scan" +ALTER TABLE tp_notnull ALTER COLUMN a SET NOT NULL, ADD CONSTRAINT c_nn NOT NULL c; +DEBUG: verifying table "tp_notnull_1" +DEBUG: verifying table "tp_notnull_2" +--debug message should contain "have been validated by using index scan" +ALTER TABLE tp_notnull ALTER COLUMN d SET NOT NULL, ADD CONSTRAINT c_nn NOT NULL c, ALTER COLUMN b SET NOT NULL; +DEBUG: all new not-null constraints on relation "tp_notnull_1" have been validated by using index scan +ALTER TABLE tp_notnull ALTER COLUMN b DROP NOT NULL; +--debug message should not contain "have been validated by using index scan" +ALTER TABLE tp_notnull ALTER COLUMN b SET NOT NULL, ALTER COLUMN b SET DATA TYPE BIGINT; +DEBUG: rewriting table "tp_notnull_1" +DEBUG: building index "tp_notnull_1_c_idx" on table "tp_notnull_1" serially +DEBUG: index "tp_notnull_1_c_idx" can safely use deduplication +DEBUG: building index "tp_notnull_1_d_idx" on table "tp_notnull_1" serially +DEBUG: index "tp_notnull_1_d_idx" can safely use deduplication +DEBUG: building index "tp_notnull_1_d_f1_idx" on table "tp_notnull_1" serially +DEBUG: building index "tp_notnull_1_f2_idx" on table "tp_notnull_1" serially +DEBUG: building index "tp_notnull_1_b_idx" on table "tp_notnull_1" serially +DEBUG: index "tp_notnull_1_b_idx" can safely use deduplication +DEBUG: rewriting table "tp_notnull_2" +DEBUG: building index "tp_notnull_2_c_idx" on table "tp_notnull_2" serially +DEBUG: index "tp_notnull_2_c_idx" can safely use deduplication +DEBUG: building index "tp_notnull_2_d_idx" on table "tp_notnull_2" serially +DEBUG: index "tp_notnull_2_d_idx" can safely use deduplication +DEBUG: building index "tp_notnull_2_d_f1_idx" on table "tp_notnull_2" serially +DEBUG: building index "tp_notnull_2_f2_idx" on table "tp_notnull_2" serially +DEBUG: building index "tp_notnull_2_b_idx" on table "tp_notnull_2" serially +DEBUG: index "tp_notnull_2_b_idx" can safely use deduplication +RESET client_min_messages; +RESET log_statement; +DROP TABLE tp_notnull; diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql index 1e9989698b6..8c8247030a1 100644 --- a/src/test/regress/sql/constraints.sql +++ b/src/test/regress/sql/constraints.sql @@ -1059,3 +1059,50 @@ COMMENT ON CONSTRAINT inv_ck ON DOMAIN constraint_comments_dom IS 'comment on in CREATE TABLE regress_notnull1 (a integer); CREATE TABLE regress_notnull2 () INHERITS (regress_notnull1); ALTER TABLE ONLY regress_notnull2 ALTER COLUMN a SET NOT NULL; + +CREATE TABLE tp_notnull (a int, b int, c int, d int, f1 int default 10, f2 int default 11) PARTITION BY range(a); +CREATE TABLE tp_notnull_1 partition of tp_notnull for values FROM ( 1 ) to (10); +CREATE TABLE tp_notnull_2 partition of tp_notnull for values FROM ( 10 ) to (21); +CREATE INDEX tp_notnull_idx_b ON tp_notnull(b); +CREATE INDEX tp_notnull_idx_c ON tp_notnull(c); +CREATE INDEX tp_notnull_idx_d ON tp_notnull(d); +CREATE INDEX tp_notnull_idx_d_include ON tp_notnull(d) include(f1); +CREATE INDEX tp_notnull_idx_f2 ON tp_notnull USING hash (f2 int4_ops); +INSERT INTO tp_notnull SELECT g, g + 10, g + 15, g + random(min=>1::int, max=>10::int) FROM generate_series(8,19) g; + +SET client_min_messages TO DEBUG1; +SET log_statement to NONE; + +--debug message should contain "have been validated by using index scan" +ALTER TABLE tp_notnull ALTER COLUMN b SET NOT NULL; +ALTER TABLE tp_notnull ALTER COLUMN b DROP NOT NULL; + +--debug message should contain "have been validated by using index scan" +ALTER TABLE tp_notnull_1 ALTER COLUMN b SET NOT NULL; + +--debug message should contain "have been validated by using index scan" +ALTER TABLE tp_notnull_2 ALTER COLUMN b SET NOT NULL; + +ALTER TABLE tp_notnull_2 ADD CONSTRAINT nn_d NOT NULL d NOT VALID; +--debug message should not contain "have been validated by using index scan" +ALTER TABLE tp_notnull_2 VALIDATE CONSTRAINT nn_d; + +--debug message should not contain "have been validated by using index scan" +ALTER TABLE tp_notnull ADD CONSTRAINT nnf1 NOT NULL f1; + +--debug message should not contain "have been validated by using index scan" +ALTER TABLE tp_notnull ADD CONSTRAINT nnf2 NOT NULL f2; + +--debug message should not contain "have been validated by using index scan" +ALTER TABLE tp_notnull ALTER COLUMN a SET NOT NULL, ADD CONSTRAINT c_nn NOT NULL c; + +--debug message should contain "have been validated by using index scan" +ALTER TABLE tp_notnull ALTER COLUMN d SET NOT NULL, ADD CONSTRAINT c_nn NOT NULL c, ALTER COLUMN b SET NOT NULL; + +ALTER TABLE tp_notnull ALTER COLUMN b DROP NOT NULL; +--debug message should not contain "have been validated by using index scan" +ALTER TABLE tp_notnull ALTER COLUMN b SET NOT NULL, ALTER COLUMN b SET DATA TYPE BIGINT; + +RESET client_min_messages; +RESET log_statement; +DROP TABLE tp_notnull; -- 2.34.1
