Skip to content

Commit ce09955

Browse files
jianhe-funhackorum
authored andcommitted
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. 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+hw@mail.gmail.com commitfest entry: https://commitfest.postgresql.org/patch/5444
1 parent 44056f6 commit ce09955

3 files changed

Lines changed: 446 additions & 5 deletions

File tree

src/backend/commands/tablecmds.c

Lines changed: 300 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
*/
218220
typedef 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,
794797
static List *collectPartitionIndexExtDeps(List *partitionOids);
795798
static void applyPartitionIndexExtDeps(Oid newPartOid, List *extDepState);
796799
static 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

Comments
 (0)