Adding a constraint to a populated PostgreSQL table can combine two distinct jobs: establish enforcement for future writes and prove that every existing row already satisfies the rule. NOT VALID separates those jobs for foreign-key and check constraints. The constraint becomes active for later inserts and updates, while verification of older rows is deferred.

That separation changes the locking profile of a schema migration without changing the intended final constraint. It also creates a temporary catalog state in which PostgreSQL enforces the rule but has not yet established that all stored rows satisfy it.

Constraint creation can skip the initial table scan

A regular ADD CONSTRAINT for a foreign key or check constraint normally verifies existing rows before the command completes. On a large relation, that verification can keep the DDL transaction open while the scan runs.

Adding NOT VALID skips that initial verification:

ALTER TABLE orders
ADD CONSTRAINT orders_customer_fk
FOREIGN KEY (customer_id)
REFERENCES customers(id)
NOT VALID;

The existing contents of orders are not scanned as part of this command. The constraint is recorded as not yet validated.

This does not make the constraint passive. Rows inserted or updated after the constraint is added are checked against it. A new foreign-key violation is rejected even while older rows have not been verified.

The same pattern applies to check constraints:

ALTER TABLE invoices
ADD CONSTRAINT invoices_total_nonnegative
CHECK (total >= 0)
NOT VALID;

A pre-existing negative value can remain temporarily, but a later insert or update that violates the check is rejected.

Validation examines the pre-existing gap

Once old violations have been corrected, VALIDATE CONSTRAINT performs the deferred verification:

ALTER TABLE invoices
VALIDATE CONSTRAINT invoices_total_nonnegative;

PostgreSQL scans the table and marks the constraint valid only if the stored rows satisfy it. If a violation remains, validation fails and the constraint stays in its not-valid state.

The concurrency behavior differs from the initial addition. Current PostgreSQL documentation states that VALIDATE CONSTRAINT takes a SHARE UPDATE EXCLUSIVE lock on the table being altered. For a foreign key, validation also requires a ROW SHARE lock on the referenced table.

Concurrent data changes do not create an unchecked window during this scan. The constraint already applies to later inserts and updates, so validation can focus on rows that existed before enforcement began.

Validity and enforcement are separate properties

The temporary state is easier to reason about when validity is treated as evidence about the whole relation rather than as an on/off switch for write checks.

An enforced but not-yet-validated constraint says two things at once:

  • later affected writes must satisfy the constraint;
  • PostgreSQL has not yet verified every older row.

After successful validation, the second condition disappears. The database can then treat the constraint as established across the stored relation.

This distinction also explains a useful migration sequence. A constraint can be installed first, historical violations can be repaired while new violations are blocked, and validation can run after the old data is clean. The final schema rule is the same, but the expensive scan is separated from the moment enforcement begins.

The option has a defined scope

NOT VALID is not a general modifier for every table constraint form. In established PostgreSQL releases it is used with foreign-key and check constraints; version-specific additions should be checked against the target server’s documentation before a migration depends on them.

It also does not remove all locking from ALTER TABLE. The addition and later validation still acquire table locks appropriate to their operations. The benefit is narrower: the potentially long verification scan is no longer part of the initial constraint-addition command.

For schema changes on populated tables, that boundary is the central property of NOT VALID: enforcement can begin before historical verification finishes, without treating the unverified rows as if they had already passed the scan.