PostgreSQL can add a foreign key or CHECK constraint to a populated table without proving at that moment that every existing row satisfies it. With NOT VALID, the database records the constraint, enforces it against subsequent writes, and leaves a separate verification step for historical rows.
That split creates a useful migration boundary. Constraint installation changes the rules for new data immediately, while VALIDATE CONSTRAINT later establishes that pre-existing data also conforms. The two operations have different work profiles and locking behavior, so treating them as one indivisible schema change can hide an important operational distinction.
NOT VALID changes the initial verification obligation
A conventional constraint addition must establish that the table already satisfies the new rule. On a large relation, proving that fact can require examining existing rows before the ALTER TABLE completes.
For supported constraint types, PostgreSQL accepts NOT VALID:
ALTER TABLE invoice
ADD CONSTRAINT invoice_customer_fk
FOREIGN KEY (customer_id)
REFERENCES customer (customer_id)
NOT VALID;The constraint exists after this statement commits, but PostgreSQL marks it as not yet validated for the table’s historical contents. This status does not mean that the constraint is disabled.
Rows inserted or updated after installation are checked against the foreign key in the ordinary way. A new row cannot use the unvalidated status as permission to introduce another violation. The deferred work concerns rows that were already present when the constraint was added.
This distinction makes NOT VALID different from mechanisms that globally suspend constraint enforcement. It narrows the unresolved question to a bounded population of existing data.
Installation and validation expose different lock profiles
Adding a constraint is still a schema operation and requires table locking. NOT VALID does not make the initial ALTER TABLE free of lock acquisition, nor does it guarantee that the statement can proceed immediately in a busy system. Conflicting transactions can delay acquisition of the required lock.
Its value is that the initial operation can avoid the full-table verification scan that a validated constraint addition would otherwise require. The expensive examination of old rows can move to a later statement:
ALTER TABLE invoice
VALIDATE CONSTRAINT invoice_customer_fk;PostgreSQL performs validation with a lock mode designed to permit ordinary reads and writes to continue on the constrained table. The operation can still interact with other schema changes and lock holders, and foreign-key validation also involves the referenced relation. The exact blocking surface therefore depends on concurrent activity rather than on the table scan alone.
Separating the operations reduces the amount of work performed while the stronger installation lock is held. It does not eliminate coordination or make validation invisible to the workload.
Validation proves a property of the complete table
Once VALIDATE CONSTRAINT succeeds, PostgreSQL can mark the constraint as validated because both data populations are covered: historical rows passed the explicit scan, and writes admitted since installation were already subject to enforcement.
That reasoning depends on enforcement beginning before validation. If new writes could bypass the rule while the historical scan ran, a successful scan would say nothing about rows committed concurrently. PostgreSQL’s split model closes that gap by making the constraint active for new changes from installation onward.
The resulting state transition is monotonic in a useful sense. Before validation, the database has a rule for future writes plus an unresolved claim about old rows. After successful validation, the unresolved claim has been discharged. The application-facing rule need not change between those states.
A failed validation leaves the constraint present and not validated. The failure identifies that historical data still violates the rule; it does not roll the schema back to a state in which new violations are accepted.
CHECK and foreign-key cases share the boundary, not every mechanism
NOT VALID is available for selected constraint forms rather than as a universal modifier for all constraints. PostgreSQL supports it for foreign-key and CHECK constraints in the relevant ALTER TABLE ... ADD ... forms. Unique and primary-key enforcement use index-backed mechanisms and follow different creation paths.
For a CHECK constraint, validation evaluates the stored expression against existing rows according to PostgreSQL’s check-constraint semantics. A check condition is satisfied when it evaluates to true or null, so a rule intended to reject nulls must state that requirement separately, commonly through NOT NULL.
For a foreign key, validation establishes referential consistency between existing referencing rows and the referenced key. Subsequent inserts and updates were already checked after the constraint was installed, but historical referencing rows remain the population that must be certified.
The shared migration pattern is therefore about validation timing. It should not be read as evidence that CHECK expressions and foreign-key references use identical enforcement internals.
Planner implications depend on validation state
Constraint metadata can affect query planning only when PostgreSQL is entitled to rely on the claimed property in the relevant optimization. An unvalidated constraint is deliberately weaker metadata: the catalog records a rule that is enforced for new changes, while historical rows have not yet been certified.
That difference matters when schema metadata is used as a statement about all rows rather than merely as a write-time guard. Validation upgrades the constraint from a partially established invariant to one PostgreSQL has checked across the existing relation.
Applications should make the same distinction. The presence of a constraint name in catalog metadata does not by itself prove that every stored row was validated. Tooling that audits invariants needs to inspect validation state rather than treating existence and full certification as synonyms.
Historical violations become migration state
A failed validation is often more informative than a failed immediate constraint addition because the schema can remain in a state that prevents further drift while remediation proceeds.
Suppose old invoices contain customer identifiers with no matching customer row. Installing the foreign key as NOT VALID prevents newly checked writes from adding more such references. Validation can then expose the remaining historical inconsistency. After those rows are corrected or otherwise resolved, the same constraint can be validated without replacing it.
This creates a clear boundary between prevention and repair. Prevention can begin at constraint installation; repair concerns the finite set of violations inherited from earlier data.
That boundary is especially relevant when application releases and data cleanup cannot be compressed into one transaction. The database can enforce the target rule for new mutations before every historical exception has been removed, provided the chosen constraint type supports this mode and the application can tolerate the temporarily unvalidated catalog state.
Validation timing is not constraint deferral
NOT VALID is easy to confuse with a deferrable constraint because both alter the timing of a check, but they move different boundaries.
A deferrable constraint changes when violations created by the current transaction must be resolved. Depending on its mode, enforcement can move from a statement boundary to a later point in that transaction.
An unvalidated constraint instead distinguishes historical rows from subsequent writes. New writes remain subject to the constraint; only certification of the rows that predate installation is postponed. VALIDATE CONSTRAINT is a schema-maintenance operation, not a request to defer current-transaction violations until commit.
The difference is visible in failure behavior. A new write that violates an installed NOT VALID foreign key can fail immediately under normal foreign-key enforcement. A validation failure, by contrast, reports that old data prevents the catalog from promoting the constraint to validated status.
The catalog state is part of the migration contract
A migration that uses NOT VALID has two durable milestones rather than one. The first establishes enforcement for new changes. The second certifies the existing relation and records that certification in constraint metadata.
Systems that deploy the first milestone but never complete the second remain in a legitimate PostgreSQL state, but not an equivalent one. Historical violations may still exist, and software inspecting schema invariants can observe that the constraint is unvalidated.
For that reason, validation is not merely cleanup after a successful migration. It completes the claim that the constraint describes the table as a whole.
The mechanism is precise: NOT VALID separates installation from historical verification while preserving enforcement on subsequent writes. Its operational benefit comes from moving a potentially substantial scan away from the stronger installation phase, and its semantic cost is an explicit interval in which the database has not yet certified all stored rows against the new invariant.