A Generated Column Is a Computed Column
A generated column is a column whose value comes from an expression, not from an INSERT. You declare the formula once in CREATE TABLE, and SQLite handles the rest. You never write to it — trying to is an error.
The shortest possible example:
total was never inserted, but it shows up in every row. SQLite recomputes it from price + tax whenever you read the row. Update either column and total follows.
The keyword phrase GENERATED ALWAYS AS is required. The ALWAYS is a SQL standard formality — there's no other option in SQLite.
VIRTUAL vs STORED
Every generated column is one of two flavors. The default is VIRTUAL:
The mental model:
VIRTUAL— costs zero bytes on disk, costs CPU on every read. Cheap to add, cheap to change later.STORED— costs disk space, costs nothing extra to read. Pays off when the expression is expensive or the column is read far more often than written.
If you don't write a keyword, you get VIRTUAL. That's almost always the right default.
Why Bother? Indexable Derived Values
The killer feature is that you can put an index on a generated column. That gives you fast lookups on derived values without rewriting every query.
Say you want case-insensitive email lookups:
The index covers the lowercased form. A query that filters on email_lower uses the index directly. SQLite even has expression indexes (CREATE INDEX ... ON users(lower(email))), but a generated column makes the derived value visible as a real column you can SELECT, reference in views, and reuse from application code.
Pulling Values Out of JSON
Generated columns shine on top of JSON. SQLite's JSON support gives you ->> to extract a scalar; wrap that in a generated column and you've got a typed, indexable field on top of a flexible blob.
user_id and kind look like regular columns to your queries, but the data lives in payload. Change the JSON, and the columns update. The index on user_id makes the lookup fast.
Rules and Constraints
A few things SQLite enforces — worth knowing before you hit them:
- The expression must be deterministic.
random(),datetime('now'), and other non-deterministic functions are not allowed. The value has to be reproducible from the row. - The expression can only reference columns in the same row. No subqueries, no aggregates, no other tables.
- You can't
INSERTorUPDATEa generated column directly.INSERT INTO products (total) VALUES (5)is an error. STOREDcolumns can't be added withALTER TABLE ... ADD COLUMN. OnlyVIRTUALcan be added after the fact.- Generated columns can have
NOT NULL,CHECK,UNIQUE, and evenFOREIGN KEYconstraints. They behave like any other column for those purposes.
A quick demo of the write rule:
sqlite> INSERT INTO products (price, tax, total) VALUES (10, 1, 999);
Runtime error: cannot INSERT into generated column "total"
The fix is to drop the generated column from the INSERT list and let SQLite compute it.
Choosing VIRTUAL or STORED
The decision usually comes down to read/write ratio and expression cost:
Rules of thumb:
- Default to
VIRTUAL. It's free at write time and fine for almost anything. - Switch to
STOREDwhen you index the column on a write-heavy table (the index needs the value persisted anyway), or when the expression is genuinely expensive. - Don't agonize over it. The flavor is part of the schema, but you can drop and recreate the column if you change your mind — at least for
VIRTUAL.
Generated Columns vs Views
There's overlap with views: both expose computed values without storing them (well, sometimes). The split is usually:
- A generated column belongs to one row and one table. Use it for per-row derivations — formatting an email, extracting a JSON field, computing a total.
- A view is a saved query. Use it when the computation involves joins, aggregation, or filtering across rows.
You can mix them. A view can SELECT from a table that has generated columns and join in extra context. Generated columns sit at the storage layer; views sit at the query layer.
Next: ATTACH DATABASE
Generated columns let one table compute its own values. The next page goes the other direction: connecting multiple SQLite databases at once with ATTACH DATABASE, so a single query can span files.
Frequently Asked Questions
What is a generated column in SQLite?
A generated column is a column whose value is computed from an expression involving other columns in the same row. You declare it with GENERATED ALWAYS AS (expression) in CREATE TABLE. You never write to it directly — SQLite computes it for you whenever the row is read or stored.
What's the difference between VIRTUAL and STORED generated columns?
A VIRTUAL column is computed on every read and takes no space on disk — it's the default. A STORED column is computed once at write time and saved to the database file, making reads cheaper but writes slightly more expensive. Both can be indexed, but STORED is usually the right pick when the expression is heavy or the column is read far more than it's written.
Can you index a generated column in SQLite?
Yes. CREATE INDEX works on generated columns, both VIRTUAL and STORED. This is the main reason to use one — you can index a derived value (like lower(email) or a JSON field extracted with ->>) and let the query planner use that index without you rewriting every query.
Can you ALTER TABLE to add a generated column?
Yes, but only for VIRTUAL columns. ALTER TABLE ... ADD COLUMN ... GENERATED ALWAYS AS (...) VIRTUAL works fine. Adding a STORED generated column with ALTER TABLE is not supported — you'd have to rebuild the table. Plan ahead if you want stored columns on existing tables.