Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit bbb96c3

Browse files
committed
Allow ALTER TABLE .. SET NOT NULL to skip provably unnecessary scans.
If existing CHECK or NOT NULL constraints preclude the presence of nulls, we need not look to see whether any are present. Sergei Kornilov, reviewed by Stephen Frost, Ildar Musin, David Rowley, and by me. Discussion: http://postgr.es/m/81911511895540@web58j.yandex.ru
1 parent 95fa9f1 commit bbb96c3

File tree

4 files changed

+195
-23
lines changed

4 files changed

+195
-23
lines changed

doc/src/sgml/ref/alter_table.sgml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,17 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
214214
<listitem>
215215
<para>
216216
These forms change whether a column is marked to allow null
217-
values or to reject null values. You can only use <literal>SET
218-
NOT NULL</literal> when the column contains no null values.
217+
values or to reject null values.
218+
</para>
219+
220+
<para>
221+
<literal>SET NOT NULL</literal> may only be applied to a column
222+
providing none of the records in the table contain a
223+
<literal>NULL</literal> value for the column. Ordinarily this is
224+
checked during the <literal>ALTER TABLE</literal> by scanning the
225+
entire table; however, if a valid <literal>CHECK</literal> constraint is
226+
found which proves no <literal>NULL</literal> can exist, then the
227+
table scan is skipped.
219228
</para>
220229

221230
<para>

src/backend/commands/tablecmds.c

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ typedef struct AlteredTableInfo
160160
/* Information saved by Phases 1/2 for Phase 3: */
161161
List *constraints; /* List of NewConstraint */
162162
List *newvals; /* List of NewColumnValue */
163-
bool new_notnull; /* T if we added new NOT NULL constraints */
163+
bool verify_new_notnull; /* T if we should recheck NOT NULL */
164164
int rewrite; /* Reason for forced rewrite, if any */
165165
Oid newTableSpace; /* new tablespace; 0 means no change */
166166
bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */
@@ -372,6 +372,9 @@ static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, LOCKMO
372372
static void ATPrepSetNotNull(Relation rel, bool recurse, bool recursing);
373373
static ObjectAddress ATExecSetNotNull(AlteredTableInfo *tab, Relation rel,
374374
const char *colName, LOCKMODE lockmode);
375+
static bool NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr);
376+
static bool ConstraintImpliedByRelConstraint(Relation scanrel,
377+
List *partConstraint, List *existedConstraints);
375378
static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,
376379
Node *newDefault, LOCKMODE lockmode);
377380
static ObjectAddress ATExecAddIdentity(Relation rel, const char *colName,
@@ -4550,10 +4553,11 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode)
45504553
else
45514554
{
45524555
/*
4553-
* Test the current data within the table against new constraints
4554-
* generated by ALTER TABLE commands, but don't rebuild data.
4556+
* If required, test the current data within the table against new
4557+
* constraints generated by ALTER TABLE commands, but don't rebuild
4558+
* data.
45554559
*/
4556-
if (tab->constraints != NIL || tab->new_notnull ||
4560+
if (tab->constraints != NIL || tab->verify_new_notnull ||
45574561
tab->partition_constraint != NULL)
45584562
ATRewriteTable(tab, InvalidOid, lockmode);
45594563

@@ -4714,13 +4718,13 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
47144718
}
47154719

47164720
notnull_attrs = NIL;
4717-
if (newrel || tab->new_notnull)
4721+
if (newrel || tab->verify_new_notnull)
47184722
{
47194723
/*
4720-
* If we are rebuilding the tuples OR if we added any new NOT NULL
4721-
* constraints, check all not-null constraints. This is a bit of
4722-
* overkill but it minimizes risk of bugs, and heap_attisnull is a
4723-
* pretty cheap test anyway.
4724+
* If we are rebuilding the tuples OR if we added any new but not
4725+
* verified NOT NULL constraints, check all not-null constraints.
4726+
* This is a bit of overkill but it minimizes risk of bugs, and
4727+
* heap_attisnull is a pretty cheap test anyway.
47244728
*/
47254729
for (i = 0; i < newTupDesc->natts; i++)
47264730
{
@@ -5749,11 +5753,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
57495753
{
57505754
/*
57515755
* If the new column is NOT NULL, and there is no missing value,
5752-
* tell Phase 3 it needs to test that. (Note we don't do this for
5753-
* an OID column. OID will be marked not null, but since it's
5754-
* filled specially, there's no need to test anything.)
5756+
* tell Phase 3 it needs to check for NULLs.
57555757
*/
5756-
tab->new_notnull |= colDef->is_not_null;
5758+
tab->verify_new_notnull |= colDef->is_not_null;
57575759
}
57585760
}
57595761

@@ -6121,8 +6123,19 @@ ATExecSetNotNull(AlteredTableInfo *tab, Relation rel,
61216123

61226124
CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
61236125

6124-
/* Tell Phase 3 it needs to test the constraint */
6125-
tab->new_notnull = true;
6126+
/*
6127+
* Ordinarily phase 3 must ensure that no NULLs exist in columns that
6128+
* are set NOT NULL; however, if we can find a constraint which proves
6129+
* this then we can skip that. We needn't bother looking if
6130+
* we've already found that we must verify some other NOT NULL
6131+
* constraint.
6132+
*/
6133+
if (!tab->verify_new_notnull &&
6134+
!NotNullImpliedByRelConstraints(rel, (Form_pg_attribute) GETSTRUCT(tuple)))
6135+
{
6136+
/* Tell Phase 3 it needs to test the constraint */
6137+
tab->verify_new_notnull = true;
6138+
}
61266139

61276140
ObjectAddressSubSet(address, RelationRelationId,
61286141
RelationGetRelid(rel), attnum);
@@ -6138,6 +6151,42 @@ ATExecSetNotNull(AlteredTableInfo *tab, Relation rel,
61386151
return address;
61396152
}
61406153

6154+
/*
6155+
* NotNullImpliedByRelConstraints
6156+
* Does rel's existing constraints imply NOT NULL for the given attribute?
6157+
*/
6158+
static bool
6159+
NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr)
6160+
{
6161+
NullTest *nnulltest = makeNode(NullTest);
6162+
6163+
nnulltest->arg = (Expr *) makeVar(1,
6164+
attr->attnum,
6165+
attr->atttypid,
6166+
attr->atttypmod,
6167+
attr->attcollation,
6168+
0);
6169+
nnulltest->nulltesttype = IS_NOT_NULL;
6170+
6171+
/*
6172+
* argisrow = false is correct even for a composite column, because
6173+
* attnotnull does not represent a SQL-spec IS NOT NULL test in such a
6174+
* case, just IS DISTINCT FROM NULL.
6175+
*/
6176+
nnulltest->argisrow = false;
6177+
nnulltest->location = -1;
6178+
6179+
if (ConstraintImpliedByRelConstraint(rel, list_make1(nnulltest), NIL))
6180+
{
6181+
ereport(DEBUG1,
6182+
(errmsg("existing constraints on column \"%s\".\"%s\" are sufficient to prove that it does not contain nulls",
6183+
RelationGetRelationName(rel), NameStr(attr->attname))));
6184+
return true;
6185+
}
6186+
6187+
return false;
6188+
}
6189+
61416190
/*
61426191
* ALTER TABLE ALTER COLUMN SET/DROP DEFAULT
61436192
*
@@ -14416,8 +14465,7 @@ PartConstraintImpliedByRelConstraint(Relation scanrel,
1441614465
{
1441714466
List *existConstraint = NIL;
1441814467
TupleConstr *constr = RelationGetDescr(scanrel)->constr;
14419-
int num_check,
14420-
i;
14468+
int i;
1442114469

1442214470
if (constr && constr->has_not_null)
1442314471
{
@@ -14451,6 +14499,27 @@ PartConstraintImpliedByRelConstraint(Relation scanrel,
1445114499
}
1445214500
}
1445314501

14502+
return ConstraintImpliedByRelConstraint(scanrel, partConstraint, existConstraint);
14503+
}
14504+
14505+
/*
14506+
* ConstraintImpliedByRelConstraint
14507+
* Do scanrel's existing constraints imply the given constraint?
14508+
*
14509+
* testConstraint is the constraint to validate. provenConstraint is a
14510+
* caller-provided list of conditions which this function may assume
14511+
* to be true. Both provenConstraint and testConstraint must be in
14512+
* implicit-AND form, must only contain immutable clauses, and must
14513+
* contain only Vars with varno = 1.
14514+
*/
14515+
bool
14516+
ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *provenConstraint)
14517+
{
14518+
List *existConstraint = list_copy(provenConstraint);
14519+
TupleConstr *constr = RelationGetDescr(scanrel)->constr;
14520+
int num_check,
14521+
i;
14522+
1445414523
num_check = (constr != NULL) ? constr->num_check : 0;
1445514524
for (i = 0; i < num_check; i++)
1445614525
{
@@ -14481,13 +14550,13 @@ PartConstraintImpliedByRelConstraint(Relation scanrel,
1448114550
/*
1448214551
* Try to make the proof. Since we are comparing CHECK constraints, we
1448314552
* need to use weak implication, i.e., we assume existConstraint is
14484-
* not-false and try to prove the same for partConstraint.
14553+
* not-false and try to prove the same for testConstraint.
1448514554
*
1448614555
* Note that predicate_implied_by assumes its first argument is known
14487-
* immutable. That should always be true for partition constraints, so we
14488-
* don't test it here.
14556+
* immutable. That should always be true for both NOT NULL and
14557+
* partition constraints, so we don't test it here.
1448914558
*/
14490-
return predicate_implied_by(partConstraint, existConstraint, true);
14559+
return predicate_implied_by(testConstraint, existConstraint, true);
1449114560
}
1449214561

1449314562
/*

src/test/regress/expected/alter_table.out

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,8 @@ insert into atacc1 (test2, test) values (1, NULL);
10281028
ERROR: null value in column "test" violates not-null constraint
10291029
DETAIL: Failing row contains (null, 1).
10301030
drop table atacc1;
1031+
-- we want check if not null was implied by constraint
1032+
set client_min_messages to 'debug1';
10311033
-- alter table / alter column [set/drop] not null tests
10321034
-- try altering system catalogs, should fail
10331035
alter table pg_class alter column relname drop not null;
@@ -1043,15 +1045,19 @@ ERROR: relation "non_existent" does not exist
10431045
-- test checking for null values and primary key
10441046
create table atacc1 (test int not null);
10451047
alter table atacc1 add constraint "atacc1_pkey" primary key (test);
1048+
DEBUG: ALTER TABLE / ADD PRIMARY KEY will create implicit index "atacc1_pkey" for table "atacc1"
1049+
DEBUG: building index "atacc1_pkey" on table "atacc1" serially
10461050
alter table atacc1 alter column test drop not null;
10471051
ERROR: column "test" is in a primary key
10481052
alter table atacc1 drop constraint "atacc1_pkey";
10491053
alter table atacc1 alter column test drop not null;
10501054
insert into atacc1 values (null);
10511055
alter table atacc1 alter test set not null;
1056+
DEBUG: verifying table "atacc1"
10521057
ERROR: column "test" contains null values
10531058
delete from atacc1;
10541059
alter table atacc1 alter test set not null;
1060+
DEBUG: verifying table "atacc1"
10551061
-- try altering a non-existent column, should fail
10561062
alter table atacc1 alter bar set not null;
10571063
ERROR: column "bar" of relation "atacc1" does not exist
@@ -1065,6 +1071,54 @@ alter table myview alter column test set not null;
10651071
ERROR: "myview" is not a table or foreign table
10661072
drop view myview;
10671073
drop table atacc1;
1074+
-- set not null verified by constraints
1075+
create table atacc1 (test_a int, test_b int);
1076+
insert into atacc1 values (null, 1);
1077+
-- constraint not cover all values, should fail
1078+
alter table atacc1 add constraint atacc1_constr_or check(test_a is not null or test_b < 10);
1079+
DEBUG: verifying table "atacc1"
1080+
alter table atacc1 alter test_a set not null;
1081+
DEBUG: verifying table "atacc1"
1082+
ERROR: column "test_a" contains null values
1083+
alter table atacc1 drop constraint atacc1_constr_or;
1084+
-- not valid constraint, should fail
1085+
alter table atacc1 add constraint atacc1_constr_invalid check(test_a is not null) not valid;
1086+
alter table atacc1 alter test_a set not null;
1087+
DEBUG: verifying table "atacc1"
1088+
ERROR: column "test_a" contains null values
1089+
alter table atacc1 drop constraint atacc1_constr_invalid;
1090+
-- with valid constraint
1091+
update atacc1 set test_a = 1;
1092+
alter table atacc1 add constraint atacc1_constr_a_valid check(test_a is not null);
1093+
DEBUG: verifying table "atacc1"
1094+
alter table atacc1 alter test_a set not null;
1095+
DEBUG: existing constraints on column "atacc1"."test_a" are sufficient to prove that it does not contain nulls
1096+
delete from atacc1;
1097+
insert into atacc1 values (2, null);
1098+
alter table atacc1 alter test_a drop not null;
1099+
-- test multiple set not null at same time
1100+
-- test_a checked by atacc1_constr_a_valid, test_b should fail by table scan
1101+
alter table atacc1 alter test_a set not null, alter test_b set not null;
1102+
DEBUG: existing constraints on column "atacc1"."test_a" are sufficient to prove that it does not contain nulls
1103+
DEBUG: verifying table "atacc1"
1104+
ERROR: column "test_b" contains null values
1105+
-- commands order has no importance
1106+
alter table atacc1 alter test_b set not null, alter test_a set not null;
1107+
DEBUG: verifying table "atacc1"
1108+
ERROR: column "test_b" contains null values
1109+
-- valid one by table scan, one by check constraints
1110+
update atacc1 set test_b = 1;
1111+
alter table atacc1 alter test_b set not null, alter test_a set not null;
1112+
DEBUG: verifying table "atacc1"
1113+
alter table atacc1 alter test_a drop not null, alter test_b drop not null;
1114+
-- both column has check constraints
1115+
alter table atacc1 add constraint atacc1_constr_b_valid check(test_b is not null);
1116+
DEBUG: verifying table "atacc1"
1117+
alter table atacc1 alter test_b set not null, alter test_a set not null;
1118+
DEBUG: existing constraints on column "atacc1"."test_b" are sufficient to prove that it does not contain nulls
1119+
DEBUG: existing constraints on column "atacc1"."test_a" are sufficient to prove that it does not contain nulls
1120+
drop table atacc1;
1121+
reset client_min_messages;
10681122
-- test inheritance
10691123
create table parent (a int);
10701124
create table child (b varchar(255)) inherits (parent);

src/test/regress/sql/alter_table.sql

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,9 @@ insert into atacc1 (test2, test) values (2, 3);
776776
insert into atacc1 (test2, test) values (1, NULL);
777777
drop table atacc1;
778778

779+
-- we want check if not null was implied by constraint
780+
set client_min_messages to 'debug1';
781+
779782
-- alter table / alter column [set/drop] not null tests
780783
-- try altering system catalogs, should fail
781784
alter table pg_class alter column relname drop not null;
@@ -809,6 +812,43 @@ drop view myview;
809812

810813
drop table atacc1;
811814

815+
-- set not null verified by constraints
816+
create table atacc1 (test_a int, test_b int);
817+
insert into atacc1 values (null, 1);
818+
-- constraint not cover all values, should fail
819+
alter table atacc1 add constraint atacc1_constr_or check(test_a is not null or test_b < 10);
820+
alter table atacc1 alter test_a set not null;
821+
alter table atacc1 drop constraint atacc1_constr_or;
822+
-- not valid constraint, should fail
823+
alter table atacc1 add constraint atacc1_constr_invalid check(test_a is not null) not valid;
824+
alter table atacc1 alter test_a set not null;
825+
alter table atacc1 drop constraint atacc1_constr_invalid;
826+
-- with valid constraint
827+
update atacc1 set test_a = 1;
828+
alter table atacc1 add constraint atacc1_constr_a_valid check(test_a is not null);
829+
alter table atacc1 alter test_a set not null;
830+
delete from atacc1;
831+
832+
insert into atacc1 values (2, null);
833+
alter table atacc1 alter test_a drop not null;
834+
-- test multiple set not null at same time
835+
-- test_a checked by atacc1_constr_a_valid, test_b should fail by table scan
836+
alter table atacc1 alter test_a set not null, alter test_b set not null;
837+
-- commands order has no importance
838+
alter table atacc1 alter test_b set not null, alter test_a set not null;
839+
840+
-- valid one by table scan, one by check constraints
841+
update atacc1 set test_b = 1;
842+
alter table atacc1 alter test_b set not null, alter test_a set not null;
843+
844+
alter table atacc1 alter test_a drop not null, alter test_b drop not null;
845+
-- both column has check constraints
846+
alter table atacc1 add constraint atacc1_constr_b_valid check(test_b is not null);
847+
alter table atacc1 alter test_b set not null, alter test_a set not null;
848+
drop table atacc1;
849+
850+
reset client_min_messages;
851+
812852
-- test inheritance
813853
create table parent (a int);
814854
create table child (b varchar(255)) inherits (parent);

0 commit comments

Comments
 (0)