October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk6 min

Adding a NOT NULL Column to a Large PostgreSQL Table: Constant Default, Backfill, or NOT VALID?

For a large PostgreSQL table, use a constant default only when every old row should get the same value. For row-specific data, backfill in stages; PostgreSQL 18 can defer validation of a NOT NULL constraint.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the migration based on what existing rows should mean. If every old row should receive the same non-volatile value, PostgreSQL 11 and later can add the column with a constant default without immediately rewriting the table. If each row needs its own value, add the column as nullable, backfill it in controlled batches, then enforce NOT NULL. PostgreSQL 18 also lets you add a NOT NULL constraint as NOT VALID, so new writes are checked before existing rows are validated; PostgreSQL 17 does not document that syntax.

Choose the path by the data, not by the fastest DDL

A fast schema change is useful only if it assigns correct values. Before writing the migration, decide what the new column should contain for historical rows and what concurrent inserts should receive. Then check the deployed PostgreSQL major version: the constant-default fast path dates to PostgreSQL 11, while NOT NULL ... NOT VALID is a PostgreSQL 18 feature.

Approach Use it when Main tradeoff Version note
Non-volatile constant default All existing rows should receive the same value. Fast addition does not make an incorrect historical value correct; volatile defaults require per-row evaluation. Metadata fast path is available in PostgreSQL 11 and later. PostgreSQL: Modifying Tables
Nullable column, backfill, then enforce Existing values must be derived from each row or otherwise differ. Backfill is real write work; batch size and pacing depend on the workload. Use across versions; check the deployed version’s ALTER TABLE behavior.
NOT NULL NOT VALID, then validate New writes must be rejected if null while historical rows are checked later. Validation still scans existing rows and has a documented lock impact. PostgreSQL 18. PostgreSQL 18 release notes
Valid CHECK, then SET NOT NULL On PostgreSQL 17 and older documented behavior, a validated check can prove no nulls remain. The check must first be validated; this is not the same as skipping validation of historical data. PostgreSQL 17 documents this scan optimization. PostgreSQL 17 ALTER TABLE

For any route, account for five things: accepted syntax on your server version, whether old rows share one value, the value future inserts receive, scan and lock effects, and whether enforcement can be separated from historical validation.

When a constant default is appropriate

PostgreSQL 11 and later can add a column with a non-volatile constant default by recording the value in metadata for existing rows instead of immediately rewriting every row. Reads of old tuples return that value; a later table rewrite can materialize it physically. This makes the ADD COLUMN operation much faster than a row-by-row update, but it is not a backfill and it does not calculate a distinct historical value for each row. See the PostgreSQL documentation on modifying tables.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For example, if every existing record truly should have status 'legacy', a uniform default may be valid. If status depends on the record’s history, account, or creation date, assigning the same value merely to avoid a rewrite would misstate the data.

ALTER TABLE target_table
  ADD COLUMN status text NOT NULL DEFAULT 'legacy';

Use only a non-volatile constant for this fast path. PostgreSQL identifies clock_timestamp() as an example of a volatile default: its value must be calculated for each row, so it follows a per-row path rather than the constant metadata optimization. Also distinguish the historical default from future behavior: changing or dropping a column default later affects subsequent inserts, not the values already represented for old rows.

Even a metadata-fast addition is not a promise of a lock-free migration or a guaranteed runtime. PostgreSQL documents lock modes by operation, and lock acquisition itself can affect deployment timing. Test the exact operation on the target major version and account for concurrent workload.

When existing rows need different values, backfill in stages

Use a staged migration when the correct value is row-specific, computed from existing data, or cannot be represented by one legitimate constant. First add the column nullable. Deploy or update application writers so inserts and relevant updates populate it. Then fill historical rows with the proper expression in bounded, repeatable batches, check that no nulls remain, and enforce the constraint.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
ALTER TABLE target_table ADD COLUMN new_column desired_type;

The following stages are a deployment outline, not a single transaction to run unchanged. The batch query depends on the table’s key and the expression that defines the correct value:

  1. Make future writes safe. Deploy code that supplies new_column for new and changed rows, or define an appropriate future default if one is semantically correct.
  2. Backfill existing rows. Update bounded batches where new_column IS NULL, calculating the value from each row’s actual data. Make the operation safe to retry, and tune batch size and pacing against observed load; PostgreSQL documentation does not prescribe a universally safe batch size.
  3. Check completion. Confirm no nulls remain and that concurrent writers cannot reintroduce them.
  4. Enforce non-nullness. Use ALTER TABLE ... ALTER COLUMN ... SET NOT NULL, or stage enforcement and validation with the version-appropriate method below.
ALTER TABLE target_table
  ALTER COLUMN new_column SET NOT NULL;

Do not substitute an arbitrary placeholder just to make this step easy. The database constraint can guarantee non-nullness, but only your migration logic can ensure the stored values are meaningful.

What NOT VALID changes—and what it does not

NOT VALID separates checking old rows from enforcing a constraint on later writes. PostgreSQL’s documentation says: “With NOT VALID, the ADD CONSTRAINT command does not scan the table and can be committed immediately.” Inserts and updates made after the constraint is installed are still checked; a later VALIDATE CONSTRAINT checks the rows that existed beforehand. Validation is a scan and takes a SHARE UPDATE EXCLUSIVE lock. See PostgreSQL 18 ALTER TABLE.

PostgreSQL 18 adds this facility for NOT NULL. If the column has already been populated for historical rows, or you want to prevent new nulls while completing the old-row check, the staged form is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
ALTER TABLE target_table
  ADD CONSTRAINT target_table_new_column_nn
  NOT NULL new_column NOT VALID;

ALTER TABLE target_table
  VALIDATE CONSTRAINT target_table_new_column_nn;

Use the exact syntax supported by the deployed major version and rehearse the migration in a representative environment. NOT VALID does not waive validation: the historical scan still happens in the second step, and the constraint cannot be considered fully validated until that succeeds.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

PostgreSQL 17 and earlier: use a validated CHECK as proof

PostgreSQL 17’s documented NOT VALID support covers CHECK and foreign-key constraints, not NOT NULL constraints. If the column is populated and you want to avoid the scan normally associated with setting the column attribute, add and validate a check that proves the column is non-null, then set NOT NULL:

ALTER TABLE target_table
  ADD CONSTRAINT target_table_new_column_nn_check
  CHECK (new_column IS NOT NULL) NOT VALID;

ALTER TABLE target_table
  VALIDATE CONSTRAINT target_table_new_column_nn_check;

ALTER TABLE target_table
  ALTER COLUMN new_column SET NOT NULL;

The check’s NOT VALID phase avoids an initial scan but still enforces the condition on new or updated rows. Validation checks existing rows. PostgreSQL 17 documents that a valid check constraint proving no null can exist lets SET NOT NULL skip its own table scan. This is a distinct version-compatible route, not PostgreSQL 18’s not-null-constraint syntax.

Plan around locks and workload

Do not describe these changes as lock-free. Lock requirements differ by operation: PostgreSQL documents that most forms of adding a table constraint require ACCESS EXCLUSIVE, with a foreign-key exception, while validation takes SHARE UPDATE EXCLUSIVE. Review the operation-specific lock notes in the manual for your server version rather than assuming that a fast metadata change has no deployment impact.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Test the exact DDL against the PostgreSQL major version in production.
  • Run a representative backfill and validation in a staging environment; measure impact on query latency, write load, and replication lag in your own setup.
  • Choose batch size and pacing from observed workload rather than copying a universal number; no general safe size or runtime is established.
  • Set operational timeouts appropriate to your deployment, and monitor the migration while it runs.
  • Keep application writers compatible with the intermediate schema, especially while the column is nullable or historical rows remain unvalidated.

The official documentation describes behavior and lock modes, but it does not establish how long a particular table’s migration will take or how much it will affect a specific workload.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.