"Virtual Generated Columns in PostgreSQL 18: Computed Columns With Zero Storage"
Can you index a virtual generated column postgres? No — but PG18's virtual generated columns compute values on read with zero storage and instant ALTER TABLE ADD COLUMN.
PostgreSQL has had stored generated columns since v12 — handy, but adding one to a big table rewrites every row and takes a lock that can stall production. PostgreSQL 18 adds virtual generated columns: the value is computed on read, stored nowhere, and ALTER TABLE ... ADD COLUMN completes in milliseconds no matter the table size. The first question everyone asks — can you index a virtual generated column postgres — has a short answer (no) and a good workaround (expression indexes). This guide covers the syntax, the virtual-vs-stored tradeoffs, the immutability rules that will bite you, and when a trigger is still the better tool. Last updated: October 4, 2026 What you'll learn Virtual vs stored generated columns: what changes in PG18 The GENERATED ALWAYS AS (...) VIRTUAL syntax Why adding a virtual column is instant (no table rewrite) The immutability rule: "generation expression is not immutable" explained Can you index a virtual generated column? (No — and the expression-index workaround) Generated column vs trigger: which to pick for derived data Virtual vs Stored Generated Columns in Postgres A generated column derives its value from other columns in the same row. PostgreSQL 18 supports two flavors: CREATE TABLE line_items ( id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY, unit_price numeric(12,2) NOT NULL, quantity integer NOT NULL, -- STORED (PG12+): computed on write, occupies disk line_total_stored numeric(12,2) GENERATED ALWAYS AS (unit_price * quantity) STORED, -- VIRTUAL (PG18+): computed on read, zero storage line_total_virtual numeric(12,2) GENERATED ALWAYS AS (unit_price * quantity) VIRTUAL ); VIRTUAL is the default in PostgreSQL 18, so GENERATED ALWAYS AS (expr) without a keyword gives you a virtual column. Stored columns pay the computation once…