Virtual, Materialized, and @generated columns

In this article, we’ll explore virtual and materialized columns and @generated fields in JSON Relational Duality Views.

Why use computed columns at all?

I’ve always computed values in a query, so what’s the point?

Indexing

In Oracle, you can index a virtual or materialized column. This is pretty fun if you frequently run queries on the computed values:

create index idx_size_category on ice_cream_sales(size_category);
Query simplification

Queries with computed columns are more concise. Logic is encapsulated in the schema and not repeated in the query.

Human error

Without computed columns, you manually calculate or store the same derived value in multiple places. Here’s the risk:

  • Someone makes a mistake and writes the wrong computed value
  • A query isn’t updated when dependent fields change, introducing errors
  • Any time you make a change, you have to hunt down everywhere you compute the value and update it. Fun, right?

Types of computed columns

Virtual Columns

Introduced in Oracle Database 11g, virtual columns are defined using an expression over other columns in a table. Virtual columns are read-only, and require no storage space on disk.

Let’s check out an example, defining virtual columns on a table:

CREATE TABLE ice_cream_sales (
    sale_id      NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    flavor       VARCHAR2(50),
    scoops       NUMBER,
    price_per_scoop NUMBER(5,2),
    -- Virtual column: Calculate total price automatically
    total_price  AS (scoops * price_per_scoop) VIRTUAL,
    -- Virtual column: Automatic size category assignment
    size_category AS (
        CASE 
            WHEN scoops = 1 THEN 'Single Scoop'
            WHEN scoops = 2 THEN 'Double Scoop'
            WHEN scoops = 3 THEN 'Triple Threat'
            ELSE 'Monster Cone'
        END
    ) VIRTUAL
);

Next, insert some data and query the virtual columns:

INSERT INTO ice_cream_sales SET
    flavor = 'Mint Chip',
    scoops = '2',
    price_per_scoop = 3.50;
INSERT INTO ice_cream_sales SET
    flavor = 'Cookies and Cream',
    scoops = '3',
    price_per_scoop = 2.75;
SELECT flavor, total_price, size_category
FROM ice_cream_sales;

The output contains the virtual columns, computed during read:

FLAVORTOTAL_PRICESIZE_CATEGORY
Mint Chip7Double Scoop
Cookies and Cream8.25Triple Thread

Virtual columns are a nice enhancement if you find yourself including the same computed fields in multiple queries. Your logic can live in the table, instead of being spread around application code.

However, virtual columns come with a big downside. Because virtual columns use no storage on disk, they’re computed on read. If you frequently run queries on virtual columns, your database computes these values again and again.

This effectively trades compute time for storage space.

Materialized Columns

Enter materialized columns, introduced in Oracle Database 23.7 as an enhancement to virtual columns.

Unlike virtual columns, materialized columns are stored on disk and computed during writes. Defining a materialized column is almost identical to a virtual column – we just replace the VIRTUAL keyword with MATERIALIZED:

CREATE TABLE ice_cream_sales (
    sale_id      NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    flavor       VARCHAR2(50),
    scoops       NUMBER,
    price_per_scoop NUMBER(5,2),
    -- Materialized column: compute and store total_price on disk
    total_price  AS (scoops * price_per_scoop) MATERIALIZED,
    -- Materialized column: compute and store size_category on disk
    size_category AS (
        CASE 
            WHEN scoops = 1 THEN 'Single Scoop'
            WHEN scoops = 2 THEN 'Double Scoop'
            WHEN scoops = 3 THEN 'Triple Threat'
            ELSE 'Monster Cone'
        END
    ) MATERIALIZED
);

Materialized columns flip the tradeoff, using more storage to save compute cycles.

And now, @generated

JSON Relational Duality Views use the @generated annotation for computed fields. If you’re not familiar, duality views add document database capabilities to your relational schemas.

@generated fields are defined using either SQL or JSONPath syntax. We’ll create a duality view that uses both:

create json relational duality view ice_cream_sales_dv
 as ice_cream_sales @insert @update @delete {
    _id : sale_id
    flavor
    scoops
    pricePerScoop : price_per_scoop 
    -- JSONPath syntax, total_price
    totalPrice @generated (path : "$.scoops * $.pricePerScoop")
    -- SQL syntax, size_category
    sizeCategory @generated (sql : "CASE 
            WHEN scoops = 1 THEN 'Single Scoop'
            WHEN scoops = 2 THEN 'Double Scoop'
            WHEN scoops = 3 THEN 'Triple Threat'
            ELSE 'Monster Cone'
        END")
};

If we select a row from the view, we’ll see a JSON document like this:

{
	"_id": 1,
	"flavor": "Mint Chip",
	"scoops": 2,
	"pricePerScoop": 3.5,
	"sizeCategory": "Double Scoop",
	"totalPrice": 7
}

Alternatively, you can include virtual or materialized columns as fields in a duality view.

However, duality views give flexibility when using computed values. Because we can have many duality views for a set of tables, different @generated fields can be created for the same data.

For example, I may have a read-write view to manage my data, and a read-only view with @generated fields for analytical queries.

When to use each type of computed column?

  1. VIRTUAL for values that are cheap to compute, or queried less often.
  2. MATERIALIZED for commonly queried values, or those that are expensive to compute.
  3. @generated if you need flexibility, or have many access patterns.
  4. If you don’t compute the value often, just include computed values in the query. No need to modify your table definitions or create views.

References

Leave a Reply

Discover more from andersswanson.dev

Subscribe now to keep reading and get access to the full archive.

Continue reading