📝 This is the English translation of the following article:
https://dev.classmethod.jp/articles/snowflake-try-virtual-columns/
Hi, I'm Sagara.
Snowflake's new feature, Virtual Columns, has reached GA. This feature allows you to define columns whose values are computed from an expression at query execution time, rather than storing values in the table. Derived columns that were previously implemented using Views can now be defined directly on the table itself, allowing you to replace a table-and-View pair with a single table.
https://docs.snowflake.com/en/release-notes/2026/10_19
https://docs.snowflake.com/en/sql-reference/virtual-columns
I tried out several usage patterns, so I'll walk through the steps and results below.
Feature Overview
Virtual Columns are virtual columns defined by specifying an AS (<expr>) clause in the table definition. Values are not stored in the table — Snowflake computes them from the expression at query execution time.
They are defined using CREATE TABLE or ALTER TABLE syntax.
-- Define at CREATE TABLE time
CREATE OR REPLACE TABLE <table> (
<col_name> <col_type> AS ( <expr> )
);
-- Add later with ALTER TABLE
ALTER TABLE <table> ADD COLUMN <col_name> <col_type> AS ( <expr> );
They serve a similar purpose to derived columns in a View's SELECT clause, but since they are defined on the table itself, there is no need to separately create and manage a View.
Limitations
Here is a summary of limitations and notes as of June 6, 2026.
Expressions not allowed in virtual columns:
-
Non-deterministic functions: Functions that return different values on each execution, such as
RANDOM(),CURRENT_TIMESTAMP,CURRENT_DATE,UUID_STRING, etc. -
Aggregate functions:
SUM,AVG,COUNT, etc. Virtual columns are evaluated per row, so aggregation is not possible. -
Window functions: Functions using the
OVERclause -
Subqueries: Nested
SELECTstatements - User-defined functions (UDFs): SQL, JavaScript, and external UDFs are all disallowed
-
Bind variables: Bind parameters such as
?,:1,:value, etc. -
Session variables: Variables set with
SET -
Positional column references: References like
$1,$2(the$1pseudo-column is allowed in virtual columns for external tables) - References to columns with DEFAULT values: Columns with DEFAULT values cannot be referenced in virtual column expressions. Virtual columns themselves cannot have DEFAULT values either.
Other limitations:
-
NOT NULLandCHECKconstraints cannot be set - Virtual columns cannot be used as clustering keys for a table
- Columns referenced by a virtual column (whether base columns or other virtual columns) cannot be dropped with
DROP COLUMN - When a virtual column references another virtual column, the referenced virtual column must be defined first (forward references are not allowed)
- Data type compatibility rules apply — the declared type must be compatible with the inferred type of the expression (numeric scale, string length, timestamp precision, etc.)
Known Issues:
- If a virtual column is defined with an expression that converts NULL to non-NULL (e.g.,
COALESCE) and is used in an outer join (LEFT JOIN), non-NULL values may be returned for rows on the preserved side where the join does not match. The workaround is to "not includeCOALESCEin the virtual column expression, and instead apply it in the SELECT clause."
Prerequisites
Creating the Database, Schema, and Warehouse
Create a database, schema, and warehouse for this verification. If you're using existing ones, replace them as appropriate.
USE ROLE SYSADMIN;
-- Create warehouse
CREATE WAREHOUSE IF NOT EXISTS VIRTUAL_COL_WH
WAREHOUSE_SIZE = 'XSMALL'
AUTO_SUSPEND = 60
AUTO_RESUME = TRUE;
-- Create database and schema
CREATE DATABASE IF NOT EXISTS VIRTUAL_COL_DB;
CREATE SCHEMA IF NOT EXISTS VIRTUAL_COL_DB.PUBLIC;
-- Set working context
USE WAREHOUSE VIRTUAL_COL_WH;
USE DATABASE VIRTUAL_COL_DB;
USE SCHEMA PUBLIC;
Creating Sample Tables and Inserting Data
Create a sample table orders_raw modeled after e-commerce order data, and insert test data.
CREATE OR REPLACE TABLE orders_raw (
order_id INT,
customer_name STRING,
email STRING,
order_date DATE,
product_name STRING,
quantity INT,
unit_price NUMBER(10, 2)
);
INSERT statements for test data (click to expand)
INSERT INTO orders_raw
(order_id, customer_name, email, order_date, product_name, quantity, unit_price)
VALUES
(1, 'Taro Yamada', 'taro.yamada@example.com', '2026-01-05', 'Wireless Mouse', 2, 2500.00),
(2, 'Hanako Suzuki', 'hanako.suzuki@dev.co.jp', '2026-01-07', 'Mechanical Keyboard', 1, 12800.00),
(3, 'Jiro Sato', 'jiro.sato@mail.example.org', '2026-01-10', 'USB Hub', 3, 1980.00),
(4, 'Misaki Tanaka', 'misaki.tanaka@example.com', '2026-01-12', 'Laptop Stand', 1, 4500.00),
(5, 'Kenichi Nakamura', 'kenichi.nakamura@company.jp', '2026-01-15', 'HDMI Adapter', 5, 890.00),
(6, 'Yuko Watanabe', 'yuko.watanabe@example.com', '2026-01-18', 'Webcam', 1, 8900.00),
(7, 'Makoto Ito', 'makoto.ito@dev.co.jp', '2026-01-20', 'Wireless Mouse', 1, 2500.00),
(8, 'Yumi Kato', 'yumi.kato@mail.example.org', '2026-01-22', 'Portable SSD', 2, 15800.00),
(9, 'Takashi Yoshida', 'takashi.yoshida@example.com', '2026-01-25', 'Keyboard Wrist Rest', 1, 2200.00),
(10, 'Yumiko Yamamoto', 'yumiko.yamamoto@company.jp', '2026-01-28', 'LED Desk Lamp', 1, 5500.00),
(11, 'Daisuke Matsumoto','daisuke.matsumoto@example.com', '2026-02-02', 'Mechanical Keyboard', 2, 12800.00),
(12, 'Sakura Inoue', 'sakura.inoue@dev.co.jp', '2026-02-05', 'Laptop Stand', 2, 4500.00),
(13, 'Asako Kimura', 'asako.kimura@mail.example.org', '2026-02-08', 'USB Hub', 1, 1980.00),
(14, 'Takuya Hayashi', 'takuya.hayashi@example.com', '2026-02-10', 'HDMI Adapter', 3, 890.00),
(15, 'Michiko Saito', 'michiko.saito@company.jp', '2026-02-12', 'Portable SSD', 1, 15800.00),
(16, 'Hiroshi Shimizu', 'hiroshi.shimizu@example.com', '2026-02-15', 'Webcam', 2, 8900.00),
(17, 'Mina Yamaguchi', 'mina.yamaguchi@dev.co.jp', '2026-02-18', 'LED Desk Lamp', 3, 5500.00),
(18, 'Masaru Ikeda', 'masaru.ikeda@mail.example.org', '2026-02-20', 'Wireless Mouse', 4, 2500.00),
(19, 'Akari Hashimoto', 'akari.hashimoto@example.com', '2026-02-22', 'Keyboard Wrist Rest', 2, 2200.00),
(20, 'Yuta Ishikawa', 'yuta.ishikawa@company.jp', '2026-02-25', 'Mechanical Keyboard', 1, 12800.00),
(21, 'Sakura Maeda', 'sakura.maeda@example.com', '2026-03-01', 'Laptop Stand', 3, 4500.00),
(22, 'Sho Fujita', 'sho.fujita@dev.co.jp', '2026-03-04', 'Portable SSD', 2, 15800.00);
You're good to go once 22 rows have been inserted.
Let's Try It Out
1. Basic: Creating a Virtual Column with Arithmetic
As the simplest pattern, let's add a virtual column total_price that calculates the total amount using quantity × unit_price via ALTER TABLE.
ALTER TABLE orders_raw
ADD COLUMN total_price NUMBER(12, 2) AS ((quantity * unit_price)::NUMBER(12, 2));
After adding it, run a SELECT to verify.
SELECT order_id, customer_name, quantity, unit_price, total_price
FROM orders_raw
ORDER BY order_id
LIMIT 5;
You're good if total_price shows the result of quantity × unit_price. The value is not stored in the table — it's computed on every query.
2. Referencing a Virtual Column from Another Virtual Column (Chained Definition)
Next, let's add a virtual column tax_amount (10% tax) that references the previously defined virtual column total_price.
ALTER TABLE orders_raw
ADD COLUMN tax_amount NUMBER(12, 2) AS ((total_price * 0.1)::NUMBER(12, 2));
SELECT order_id, total_price, tax_amount
FROM orders_raw
ORDER BY order_id
LIMIT 5;
You're good if tax_amount equals total_price × 0.1. As shown here, you can define a virtual column that references another previously defined virtual column.
3. Creating a Derived Column with String Functions
As an example of string manipulation, let's add a virtual column email_domain that extracts the email domain from the email column. We use POSITION to find the position of @ and SUBSTR to extract the string after it.
ALTER TABLE orders_raw
ADD COLUMN email_domain STRING AS (SUBSTR(email, POSITION('@', email) + 1));
SELECT customer_name, email, email_domain
FROM orders_raw
ORDER BY order_id
LIMIT 5;
You're good if the email_domain column shows domain parts like example.com or dev.co.jp. Deterministic built-in string functions like SUBSTR and POSITION can be used in virtual column expressions.
4. Creating a Classification Column with a CASE Expression
Let's use a CASE expression to add a virtual column price_tier that classifies orders by total amount. It references the virtual column total_price to determine the price bracket.
ALTER TABLE orders_raw
ADD COLUMN price_tier STRING AS (
CASE
WHEN total_price >= 20000 THEN 'high'
WHEN total_price >= 5000 THEN 'mid'
ELSE 'low'
END
);
SELECT order_id, total_price, price_tier
FROM orders_raw
ORDER BY total_price DESC;
You're good if orders are classified as high, mid, or low based on the total_price value.
5. Identifying Virtual Columns with DESC TABLE / SHOW COLUMNS
Let's check the table definition. Using DESC TABLE, you can identify whether a column is a regular column or a virtual column via the KIND column.
DESC TABLE orders_raw;
Regular columns show COLUMN in the KIND column, while virtual columns show VIRTUAL. You're good if the EXPRESSION column contains the expression you defined.
You can also check the same information with SHOW COLUMNS. Here, the kind column shows VIRTUAL_COLUMN and the expression column contains the expression.
SHOW COLUMNS IN TABLE orders_raw;
6. Creating a Table with Virtual Columns Using CTAS
You can also create a table with virtual columns using CREATE TABLE AS SELECT (CTAS). Since virtual columns are not counted in the number of columns in the SELECT clause, you only need to specify the non-virtual columns in the SELECT.
Let's create an aggregated table with a category name and tax-included price (virtual column) by JOINing a product category master with orders_raw.
First, create the product category master table.
CREATE OR REPLACE TABLE product_categories (
product_name STRING,
category STRING
);
INSERT INTO product_categories VALUES
('Wireless Mouse', 'Pointing Device'),
('Mechanical Keyboard', 'Input Device'),
('USB Hub', 'Peripheral'),
('Laptop Stand', 'Accessory'),
('HDMI Adapter', 'Converter'),
('Webcam', 'Video Equipment'),
('Portable SSD', 'Storage'),
('Keyboard Wrist Rest', 'Accessory'),
('LED Desk Lamp', 'Lighting');
Next, create the table with the virtual column tax_included_price (tax-inclusive price) using CTAS.
Here is the continuation from Section 6:
CREATE OR REPLACE TABLE orders_with_category (
order_id INT,
customer_name STRING,
product_name STRING,
category STRING,
total_price NUMBER(12, 2),
tax_included_price NUMBER(12, 2) AS ((total_price * 1.1)::NUMBER(12, 2))
)
AS SELECT o.order_id, o.customer_name, o.product_name, c.category, o.total_price
FROM orders_raw o
LEFT JOIN product_categories c ON o.product_name = c.product_name;
SELECT * FROM orders_with_category ORDER BY order_id LIMIT 5;
You're good if tax_included_price shows the tax-inclusive amount of total_price × 1.1. Virtual columns can be defined without any issues in CTAS as well.
7. Testing Disallowed Expressions to Confirm Errors
Let's actually try using expressions that are not allowed in virtual columns and check the resulting error messages.
Non-deterministic Function (RANDOM)
ALTER TABLE orders_raw
ADD COLUMN random_val INT AS (RANDOM());
NOT NULL Constraint
ALTER TABLE orders_raw
ADD COLUMN total_price2 NUMBER(12, 2) AS ((quantity * unit_price)::NUMBER(12, 2)) NOT NULL;
DROP COLUMN on a Dependent Column
Attempting to DROP COLUMN on a column that a virtual column depends on will be blocked.
ALTER TABLE orders_raw DROP COLUMN quantity;
Closing Thoughts
I tried out Snowflake's Virtual Columns across several different patterns.
Being able to consolidate computed columns — which previously required a "table + View" pair — directly into the table itself, reducing the number of objects to manage, is a significant benefit. Since no additional storage costs are incurred, I think this is a feature worth adopting proactively.
On the other hand, there are quite a few limitations to be aware of: the types of expressions that cannot be used (non-deterministic functions, aggregations, UDFs, etc.), the lack of support for NOT NULL / CHECK constraints, and the known issue with COALESCE × LEFT JOIN, so some caution is needed.
Since this feature has just reached GA, I'm looking forward to further improvements down the road. Give it a try!











Top comments (0)