PostgreSQL 18 can keep a generated column out of the stored row entirely. A virtual generated column evaluates its expression when the column is read, while a stored generated column evaluates on write and occupies storage like an ordinary column. PostgreSQL 18 also makes the virtual form the default when neither VIRTUAL nor STORED is specified.
That distinction changes where computation occurs and which expressions PostgreSQL accepts. It also affects triggers, inheritance, partitioning, and logical replication.
Virtual values are derived during reads
A generated column is defined with GENERATED ALWAYS AS. In PostgreSQL 18, this table uses a virtual generated column:
CREATE TABLE invoice_line (
quantity integer NOT NULL,
unit_price numeric(12, 2) NOT NULL,
line_total numeric(14, 2)
GENERATED ALWAYS AS (quantity * unit_price) VIRTUAL
);line_total does not occupy storage as a computed value. When a query reads that column, PostgreSQL evaluates the generation expression from the current base-column values.
The stored form moves that work to writes:
CREATE TABLE invoice_line_stored (
quantity integer NOT NULL,
unit_price numeric(12, 2) NOT NULL,
line_total numeric(14, 2)
GENERATED ALWAYS AS (quantity * unit_price) STORED
);Here, inserts and updates compute line_total, and the resulting value is stored on disk. The SQL interface still treats both forms as generated: applications cannot supply an arbitrary value for the generated column in INSERT or UPDATE.
Generation expressions have strict boundaries
Generated expressions are not general query expressions. PostgreSQL requires functions and operators in the expression to be immutable. The expression can reference other columns in the same row, but it cannot reference another generated column, use a subquery, or refer to another table.
Virtual columns add another restriction in PostgreSQL 18: their data type must be built in, and their expressions cannot reference user-defined functions or types, including indirect use through operators or casts. Stored generated columns do not have that particular restriction.
These rules keep virtual evaluation tied to expressions PostgreSQL can execute from the row being read without depending on mutable external state.
Defaults and generated columns have different timing
A default expression supplies a value when an insert omits a column. Once stored, that value does not automatically track later changes to other columns.
A generated expression has different semantics. Its value is derived from base columns and cannot be overridden with an arbitrary value. For a virtual column, derivation occurs when the value is read. For a stored column, derivation occurs when the row is inserted or updated.
Defaults can also use volatile expressions such as random(). Generated expressions cannot, because their functions and operators must be immutable.
Trigger visibility follows the storage choice
Stored generated columns are computed after BEFORE triggers and before AFTER triggers. A BEFORE trigger can modify base columns, and those changes feed the stored generation expression. The new stored generated value is then available to AFTER triggers.
Virtual generated columns are not computed when triggers fire. Code that depends on a generated value inside trigger execution therefore cannot treat the two storage forms as interchangeable.
Partitioned schemas must preserve the generated kind
Inheritance and partitioning require corresponding parent and child columns to agree on whether the column is generated and whether it is stored or virtual. Their generation expressions can differ.
The expression selected for evaluation also depends on the storage form. For a stored column, PostgreSQL applies the expression associated with the table that physically contains the row during a write. For a virtual column, PostgreSQL applies the expression associated with the table named by the read.
That distinction is relevant when inherited or partitioned definitions intentionally use different expressions.
Logical replication treats stored columns separately
PostgreSQL 18 can publish stored generated columns through logical replication. A publication can name stored generated columns in a column list or set publish_generated_columns to stored.
Virtual generated columns are not published as generated values. They are computed at read time, so there is no stored generated value to transmit in the same manner.
Even for stored columns, publication behavior needs matching subscriber design. When both publisher and subscriber columns are generated, the subscriber normally computes its own generated value. PostgreSQL documents separate rules for cases where a published stored generated value targets a regular subscriber column.
Virtual generated columns therefore shift more than disk usage. They place expression evaluation on the read path and impose tighter expression constraints, while stored columns retain computed data and participate in write-time behavior that virtual columns do not. The choice belongs in schema design alongside query patterns, trigger behavior, partition definitions, and replication requirements.