Generated Columns
MySQL generated columns: VIRTUAL and STORED. Compute fields from other columns; index them; keep schemas tidy.
MySQL — generated columns
EXAMPLE
-- ===== Syntax ===== CREATE TABLE orders ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, subtotal_cents BIGINT NOT NULL, tax_cents BIGINT NOT NULL, total_cents BIGINT GENERATED ALWAYS AS (subtotal_cents + tax_cents) VIRTUAL, total_aud DECIMAL(12,2) GENERATED ALWAYS AS ((subtotal_cents + tax_cents) / 100) STORED ); -- VIRTUAL: computed on read; no storage cost (default) -- STORED: computed on write; persisted on disk INSERT INTO orders (subtotal_cents, tax_cents) VALUES (4500, 495); SELECT total_cents, total_aud FROM orders; -- both available -- ===== Use cases ===== -- 1. Compute a value once, query it many times -- 2. Derive an index-friendly representation of JSON -- 3. Enforce a derived constraint via CHECK on the generated column -- ===== Index a JSON path ===== CREATE TABLE profiles ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, data JSON NOT NULL, country VARCHAR(2) GENERATED ALWAYS AS (data->>'$.country') STORED, INDEX (country) ); SELECT * FROM profiles WHERE country = 'AU'; -- Uses the index. Without the generated column, you would seq-scan. -- ===== VIRTUAL + index ===== CREATE TABLE events ( ts DATETIME NOT NULL, day DATE GENERATED ALWAYS AS (DATE(ts)) VIRTUAL, INDEX (day) ); -- VIRTUAL columns CAN be indexed; index stores the computed value. -- ===== Constraints ===== ALTER TABLE orders ADD CONSTRAINT chk_positive_total CHECK (total_cents >= 0); -- ===== Limitations ===== -- - Generated columns CANNOT reference other generated columns (in most versions) -- - VIRTUAL generated columns require deterministic expressions -- - Cannot use non-deterministic functions (NOW(), RAND(), UUID()) -- - Subqueries are not allowed in the expression -- ===== When to choose VIRTUAL vs STORED ===== -- VIRTUAL: -- - Lightly-read columns; cheap to compute -- - Avoid storage cost -- - You can still index them (the index IS stored) -- STORED: -- - Expensive expression -- - Used in many queries -- - Foreign keys reference them (FK requires STORED) -- ===== Updating ===== -- You CANNOT directly assign to a generated column: UPDATE orders SET total_cents = 999 WHERE id = 1; -- ERROR -- Update the source columns; the generated value follows. -- ===== Use for migrations ===== -- During schema change, generate the new shape from the old: ALTER TABLE users ADD email_lower VARCHAR(254) GENERATED ALWAYS AS (LOWER(email)) VIRTUAL, ADD UNIQUE INDEX (email_lower); -- Existing rows immediately get the indexed lowercase email. -- ===== Patterns to internalise ===== -- - Generated columns + index for JSON path queries -- - VIRTUAL by default; STORED when read-heavy or FK target -- - Pair with CHECK constraints for derived invariants -- - Avoid non-deterministic expressions -- ===== Pitfalls ===== -- - STORED columns add storage cost; measure on large tables -- - Changing the expression requires ALTER TABLE rewrite (slow on big tables) -- - VIRTUAL column expressions run on every read — keep them cheap -- - Forgetting that NOW() / RAND() are forbidden -> error at CREATE
Why it matters
Generated columns turn schema into computed types. VIRTUAL for free, STORED for performance + FK targets. Combine with indexes on JSON paths or normalised fields (lowercase emails). The expressions stay deterministic and the schema stays the single source of truth.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
ALTER TABLE products
ADD price_with_tax DECIMAL(10,2) AS (price * 1.10) VIRTUAL;
Try it Yourself »
Discussion
Loading…