Adding a Stored Generated Column Without Long-Lived Locks — May 2026 Summary
Overview
This thread proposes a two-patch approach to allow adding stored generated columns to existing PostgreSQL tables without requiring a long-lived AccessExclusiveLock for the full duration of a table rewrite. The patches introduce new DDL syntax (ALTER COLUMN c ADD GENERATED ALWAYS AS (expr) STORED) and an optimization that skips the table rewrite when a validated CHECK constraint proves existing data already satisfies the expression.
The Problem
Adding a stored generated column currently requires a full table rewrite under AccessExclusiveLock, making the table completely inaccessible for potentially minutes or hours on large tables. No incremental migration path exists today.
Proposed Solution
Patch 1: New DDL Syntax
Introduces ALTER TABLE t ALTER COLUMN c ADD GENERATED ALWAYS AS (expr) STORED to convert an existing column into a stored generated column. By default still performs a rewrite, but provides the foundation for Patch 2.
Key implementation choices:
- Placed in
AT_PASS_SET_EXPRESSIONpass for correct dependency ordering - Partitioning/inheritance semantics mirror
DROP EXPRESSION(must apply to entire inheritance tree) - Uses
RememberAllDependentForRebuildingfor early validation andAddRelationNewConstraintsfor cooked defaults
Patch 2: Constraint-Based Rewrite Skip
If a validated CHECK constraint (c = expr) exists where expr structurally matches the generated column expression and uses a mergejoinable equality operator, the table rewrite is skipped entirely.
Intended Online Migration Pattern
- Add nullable column (lightweight lock)
- Add trigger to populate column for new/updated rows
- Add
NOT VALIDCHECK constraint - Backfill existing rows in batches
VALIDATE CONSTRAINT(ShareUpdateExclusiveLock only)ADD GENERATED ALWAYS AS (expr) STORED— skips rewrite- Drop trigger and constraint
First Review: Critical Issues Found
Laurenz Albe provided the first substantive review, identifying several bugs and design gaps:
Critical NULL Handling Bug
The rewrite-skip optimization is unsound with NULL values. Since CHECK constraints treat NULL results as "not violated," a constraint CHECK (col2 = col1) passes even when col2 IS NULL. This means the constraint does NOT prove all existing values match the expression, and the rewrite is incorrectly skipped, leaving stale NULLs in place. The reviewer suggests requiring a NOT NULL constraint on the column before allowing the optimization.
Additional Bugs
- Identity column conflict: Converting an identity column to stored generated succeeds silently instead of being rejected
- Orphaned sequences: Columns with owned sequences (e.g.,
bigserial) leave sequences orphaned after conversion
Design Consistency
Laurenz argues SET EXPRESSION should also benefit from the rewrite-skip optimization for consistency — if adding a generated expression can skip the rewrite, changing one should too.
Missing Items
- No documentation yet (ALTER TABLE page, optimization pattern description)
- No psql tab completion for new syntax
- Missing code comments in new implementation branch
- Non-standard SQL syntax flagged for awareness (acceptable but noted)
Design Decisions
| Decision | Rationale |
|---|---|
STORED keyword required |
Virtual columns don't need this optimization; explicit about data overwrite |
| Structural expression matching | Conservative but safe; avoids complex semantic equivalence |
| Mergejoinable operator requirement | Ensures proper equality semantics, not user-defined operators |
Syntax choice (ADD GENERATED) |
Mirrors identity column syntax (ADD GENERATED AS IDENTITY) |
Current Status
Patch is at v4, rebased against master. First review received with critical correctness issue (NULL handling) that needs resolution. The patch is targeting a future commitfest (proposed for in-person discussion at pgconf.dev) and explicitly not the current release cycle.