Supabase’s generated columns are Postgres’s way of making computed fields feel like first-class citizens, but they’re actually just a clever application of existing Postgres features.

Let’s see them in action. Imagine you have a products table and want to store the full product name by combining brand and model.

CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    brand TEXT NOT NULL,
    model TEXT NOT NULL,
    full_name TEXT GENERATED ALWAYS AS (brand || ' ' || model) STORED
);

INSERT INTO products (brand, model) VALUES ('Apple', 'iPhone 15 Pro');
INSERT INTO products (brand, model) VALUES ('Samsung', 'Galaxy S24 Ultra');

SELECT * FROM products;

This will output:

 id | brand   |   model        |        full_name
----+---------+----------------+--------------------------
  1 | Apple   | iPhone 15 Pro  | Apple iPhone 15 Pro
  2 | Samsung | Galaxy S24 Ultra | Samsung Galaxy S24 Ultra

The full_name column is automatically populated and updated whenever brand or model changes. It’s not magic; it’s a specific Postgres syntax: GENERATED ALWAYS AS (expression) STORED.

The core problem this solves is data redundancy and inconsistency. Without generated columns, you’d either have to:

  1. Manually update full_name in your application code every time brand or model changes. This is error-prone and requires careful synchronization.
  2. Use a trigger to update full_name. This works but adds complexity to your database logic, and triggers can sometimes be harder to reason about than declarative column definitions.
  3. Compute full_name on the fly in your SELECT queries. This is fine for simple cases, but if you frequently filter or sort by full_name, it can be less performant than having it pre-computed.

Generated columns offer a declarative, database-level solution. You define how the column is computed, and Postgres handles the rest. The GENERATED ALWAYS AS (expression) part is where you specify the computation using standard SQL. The STORED keyword is crucial: it means the computed value is physically stored on disk, just like a regular column. This makes reads very fast because the value is already there. The alternative is VIRTUAL, which computes the value on-the-fly when queried, saving disk space but potentially impacting read performance. Supabase’s generated columns are always STORED.

The system leverages Postgres’s built-in support for generated columns, which were introduced in PostgreSQL 12. Supabase simply exposes this feature through its SQL editor and client libraries, making it accessible without needing to directly manage Postgres versions or extensions. The GENERATED ALWAYS AS syntax means the column is immutable once created; you cannot directly INSERT or UPDATE the full_name column itself. Its value is solely determined by the expression.

The STORED attribute means that the computed value is written to disk. This has implications for write performance and disk space. For every row inserted or updated, the expression brand || ' ' || model is evaluated, and the result is stored. If your computation is very expensive (e.g., complex joins or function calls) or if you have extremely high write volumes, this could become a bottleneck. However, for simple concatenations like this, the performance overhead is usually negligible and far outweighed by the benefits of fast reads.

A common point of confusion is the difference between STORED and VIRTUAL generated columns. While Postgres 12+ supports both, Supabase (and by extension, the GENERATED keyword in its SQL editor) only supports STORED. VIRTUAL columns don’t take up storage space but are computed every time they are read, which can be slower for frequent reads. STORED columns take up space but are as fast to read as regular columns.

When you define a generated column, you are essentially creating a single source of truth for that derived data. This eliminates the need for application-level logic to keep related fields synchronized, reducing the chances of subtle bugs and making your data model cleaner and more robust.

If you’re using GENERATED ALWAYS AS, you’ll never need to worry about updating the generated column directly. However, you might still run into issues if the underlying columns used in the expression (brand and model in our example) are not properly constrained or validated, leading to unexpected values in your generated column.

Want structured learning?

Take the full Supabase course →