Snowflake Comparison

DuckDB Semantic Views is modeled on Snowflake’s CREATE SEMANTIC VIEW SQL DDL interface. If you have used Snowflake semantic views, much of the syntax and concept model will be familiar. This page maps the key concepts and calls out the differences.

Note

Snowflake has two distinct interfaces for semantic views: the SQL DDL (CREATE SEMANTIC VIEW) and the older YAML spec (CREATE SEMANTIC VIEW FROM YAML, designed for Cortex Analyst). All comparisons on this page target the SQL DDL interface only. The YAML spec includes concepts like time_dimensions, custom_instructions, and sample_values that exist to serve the AI SQL generation layer and have no equivalent in the SQL DDL.

Concept Mapping

Concept

Snowflake SQL DDL

DuckDB Semantic Views

Define a semantic view

CREATE SEMANTIC VIEW

CREATE SEMANTIC VIEW

Expression checking

Each expression is compiled at CREATE

Each dimension, fact and metric is bound at CREATE, and one that does not bind refuses the statement (see Members that do not bind)

Table declarations

TABLES clause with aliases

TABLES clause with aliases and optional PRIMARY KEY

Relationships

RELATIONSHIPS clause with FK REFERENCES

RELATIONSHIPS clause with FK REFERENCES

Dimensions

DIMENSIONS clause

DIMENSIONS clause

Metrics (measures)

METRICS clause

METRICS clause

View variables

VARIABLES clause, set per query with VARIABLES name = value in SEMANTIC_VIEW(...)

The same VARIABLES clause and query clause, plus variables := {...} in semantic_view() (see VARIABLES)

Reusable row-level expressions

FACTS clause

FACTS clause (queryable with FACTS in SEMANTIC_VIEW(...))

Metric composition

Derived metrics (metric referencing other metrics)

Derived metrics (same pattern)

Semi-additive metrics

SEMI ADDITIVE / NON ADDITIVE BY

NON ADDITIVE BY (see How to Use Semi-Additive Metrics)

Window function metrics

OVER clause with PARTITION BY EXCLUDING

OVER clause with PARTITION BY EXCLUDING (see How to Use Window Function Metrics)

Metadata annotations

COMMENT, WITH SYNONYMS, WITH TAG

COMMENT, WITH SYNONYMS, WITH TAG (see How to Use Metadata Annotations)

Access modifiers

PRIVATE / PUBLIC on metrics and facts; “You cannot mark a dimension as private. Dimensions are always public.”

Same rule: PRIVATE / PUBLIC on metrics and facts, PRIVATE rejected on dimensions. PUBLIC is accepted on a dimension as an explicit no-op, since it asserts what is already true

Materializations / pre-aggregation

Not part of Snowflake’s CREATE SEMANTIC VIEW DDL

MATERIALIZATIONS clause for routing to pre-aggregated tables (see How to Use Materializations)

Query interface

SEMANTIC_VIEW(...) construct, and direct SQL with semantic resolution

The same SEMANTIC_VIEW(…) construct, standard SQL with AGG, and the semantic_view() table function

Wildcard selection

alias.* in query parameters

alias.* in DIMENSIONS, METRICS, FACTS (see How to Use Wildcard Selection)

View inspection

DESCRIBE / DESC SEMANTIC VIEW

DESCRIBE SEMANTIC VIEW (DESC abbreviation also accepted)

List views

SHOW SEMANTIC VIEWS, with LIMIT <rows> FROM '<name>' to page through the list

SHOW SEMANTIC VIEWS, LIMIT ... FROM included

Terse view listing

SHOW TERSE SEMANTIC VIEWS

SHOW TERSE SEMANTIC VIEWS

Column listing

SHOW COLUMNS IN SEMANTIC VIEW

SHOW COLUMNS IN SEMANTIC VIEW

Filter by scope

IN SCHEMA / IN DATABASE

IN SCHEMA / IN DATABASE on SHOW commands

Retrieve DDL text

GET_DDL('SEMANTIC_VIEW', ...) or GET_DDL('SEMANTIC VIEW', ...)

GET_DDL, with either spelling of the object type

Alter a view

ALTER SEMANTIC VIEW

ALTER SEMANTIC VIEW (RENAME TO, SET and UNSET of COMMENT and MAX_STALENESS, SET TAG, UNSET TAG)

Drop a view

DROP SEMANTIC VIEW

DROP SEMANTIC VIEW

Syntax Alignment

The DDL syntax is intentionally close to Snowflake’s. The clause order (TABLES, RELATIONSHIPS, VARIABLES, FACTS, DIMENSIONS, METRICS) matches Snowflake, and the entry syntax within each clause follows the same pattern.

Note

Syntax conveniences for porting.

Added in version 0.11.0.

Several Snowflake spellings are accepted to reduce friction when porting DDL: the table alias in TABLES is optional (TABLES (orders PRIMARY KEY (id)), matching Snowflake’s [alias AS] table); a view-level COMMENT = '...' may appear in the trailing position after the last clause; PUBLIC is accepted on dimensions; WITH SYNONYMS (...) is accepted without the =; and DESC SEMANTIC VIEW is accepted as an abbreviation of DESCRIBE SEMANTIC VIEW. See CREATE SEMANTIC VIEW.

Added in version 0.18.0.

Snowflake DDL now pastes as written: AS before TABLES is optional; LABELS = (FILTER) may sit between a member’s name and its AS (o.is_large LABELS = (FILTER) AS o.amount > 100), where Snowflake puts it; and GET_DDL takes 'SEMANTIC VIEW' as well as 'SEMANTIC_VIEW'.

-- Snowflake writes no AS keyword before the body clauses.
CREATE SEMANTIC VIEW analytics
TABLES (
    o AS orders,
    c AS customers PRIMARY KEY (id)
)
RELATIONSHIPS (
    order_customer AS o(customer_id) REFERENCES c
)
DIMENSIONS (
    c.customer_name AS c.name,
    o.region AS o.region
)
METRICS (
    o.revenue AS SUM(o.amount)
);
-- AS is optional: the Snowflake form above also works.
CREATE SEMANTIC VIEW analytics AS
TABLES (
    o AS orders    PRIMARY KEY (id),
    c AS customers PRIMARY KEY (id)
)
RELATIONSHIPS (
    order_customer AS o(customer_id) REFERENCES c
)
DIMENSIONS (
    c.customer_name AS c.name,
    o.region AS o.region
)
METRICS (
    o.revenue AS SUM(o.amount)
);

Key Differences

Primary Key Declarations

Note

PRIMARY KEY declarations in the TABLES clause are optional at the syntax level, but any table used as the target of a RELATIONSHIPS entry must declare a PRIMARY KEY or UNIQUE key in its own TABLES entry. An explicit column list on the referencing side (REFERENCES c(id)) does not replace that declaration: it has to name columns the target already declares as its PRIMARY KEY or as one of its UNIQUE keys.

Both systems take the key from the semantic view definition rather than from the physical table. Snowflake’s CREATE SEMANTIC VIEW reference describes a referenced column as “a column identified with the PRIMARY KEY or UNIQUE constraint in the logical table definition”, and DuckDB Semantic Views applies the same rule. In both, a PRIMARY KEY in a semantic view is a logical assertion you make about the data, not a physical constraint imported from the catalog.

Changed in version 0.10.0: Automatic PK inference from DuckDB’s duckdb_constraints() catalog was removed (breaking). Earlier releases imported a native table’s physical PRIMARY KEY at CREATE time when the TABLES entry declared none; this fallback is gone. You must now declare the key explicitly, whether the table is a native DuckDB table or an external source. Migration: add a PRIMARY KEY (...) (or UNIQUE (...)) clause to any TABLES entry that previously relied on the auto-fallback.

Tip

This uniform rule is convenient for data engineers using DuckDB with Iceberg, Parquet, CSV, or Postgres sources: those catalogs never surfaced PK/FK metadata through duckdb_constraints() anyway, so declaring keys in the TABLES clause was always required for them. Now native DuckDB tables follow the same explicit-declaration rule, so there is one consistent model regardless of data source.

-- Every table used as a join target declares its key explicitly,
-- regardless of whether it is a native DuckDB table or an external source.
CREATE SEMANTIC VIEW analytics AS
TABLES (
    o AS orders    PRIMARY KEY (id),
    c AS customers PRIMARY KEY (id)
)
RELATIONSHIPS (
    order_customer AS o(customer_id) REFERENCES c
)
DIMENSIONS (c.name AS c.name)
METRICS (o.revenue AS SUM(o.amount));

If a relationship’s target table declares neither key, the extension rejects the definition at CREATE time, whether or not the REFERENCES side lists columns. With c AS customers declared without a key in the example above, the error is:

Parser Error: Table 'c' has no PRIMARY KEY declared but is referenced by FK in 'o'.
Add PRIMARY KEY (cols) or UNIQUE (cols) to the TABLES clause for c. (v0.10.0:
physical-catalog PK auto-inference removed -- see CHANGELOG.)

This prevents the extension from synthesizing an incorrect JOIN ON clause.

Query Interface

Both systems query a semantic view in three ways: by name with standard SQL, passing each metric to AGG; with the SEMANTIC_VIEW(...) construct; and, in DuckDB only, with the semantic_view() table function the construct turns into.

-- Both: standard SQL over the view by name, with AGG around each metric
SELECT region, AGG(revenue)
FROM analytics
GROUP BY region;

-- Both: the SEMANTIC_VIEW construct. The view name and the dimension /
-- metric references are bare identifiers (not string literals), and there
-- is no comma between the view name and the DIMENSIONS / METRICS keywords.
SELECT * FROM SEMANTIC_VIEW(
    analytics
    DIMENSIONS orders.region
    METRICS orders.revenue
);

-- DuckDB: the same query as a table function with named lists
SELECT * FROM semantic_view('analytics',
    dimensions := ['orders.region'],
    metrics := ['orders.revenue']
);

The standard-SQL form follows Snowflake’s rules: GROUP BY expressions are the dimensions, a metric is passed to AGG (MIN, MAX and ANY_VALUE of a metric mean the same), other aggregates are ad-hoc metrics, WHERE refers to dimensions and facts, HAVING to aggregated metrics, and the block that reads the view has no joins, window functions, QUALIFY, PIVOT or qualified correlated references. See How to Query a Semantic View with Standard SQL. Two differences come from DuckDB rather than from semantic views: a name that is also a table, a view or a CTE in scope reads that, not the semantic view; and SELECT * is refused, where the construct’s SELECT * FROM SEMANTIC_VIEW(...) is not.

Cardinality Inference

Both systems infer cardinality from constraints. In DuckDB Semantic Views, cardinality is inferred from PRIMARY KEY and UNIQUE declarations in the TABLES clause:

  • If the FK columns on the “from” side match a PK or UNIQUE constraint, the relationship is one-to-one.

  • Otherwise, the relationship is many-to-one (the default).

The extension uses inferred cardinality for fan trap detection.

What an Expression May Reference

Both systems scope a member expression to its own logical table. Snowflake’s validation rules state that expressions can refer to base table columns or other expressions on the same logical table, and that they “cannot refer to base table columns from other tables”. To reach another table you declare a relationship, define a fact on the source table, and refer to that fact from the connected table. The rule here is the same.

A named fact on another table – Snowflake’s supported cross-table form – is at parity: the fact’s expression is inlined at its reference site and its table is joined, so o.net_total AS SUM(o.amount - c.cust_discount) computes over the joined relation. One safety rule is added on top of Snowflake’s, for a hazard its single-grain model does not face here: if the fact’s table fans the referencing member’s – a fact on a child table, reached across a one-to-many edge – joining it would multiply the member’s rows, so the query is rejected with a fan trap error rather than answered with an inflated aggregate. See Reference a Fact on Another Table.

A raw column of another table (o.margin AS o.amount - c.discount) is rejected at CREATE in both systems, with an error naming the rule and pointing at the fact-based alternative. A qualifier that names no declared table at all is left to DuckDB, which is the right place for a struct path, a bound parameter, or a typo to be resolved.

Changed in version 0.12.0: Before, the definition was accepted and the reference surfaced at query time as a DuckDB unknown-alias error – the same rule, enforced later.

Derived metrics are unaffected: a metric that references metrics on other tables (m AS t1.metric_1 + t2.metric_2) is supported in both systems, and is computed per grain – see Metric Grain below.

Reported Data Types

Snowflake populates the data_type column of SHOW SEMANTIC DIMENSIONS / METRICS / FACTS (and the DATA_TYPE rows of DESCRIBE) with the member’s actual data type.

Changed in version 0.14.0: data_type is populated again. It had been empty for every view created since v0.10.0; views created in that window report their types too, with no need to re-create them.

The same holds here. The type is not stored at CREATE; it is worked out when the SHOW / DESCRIBE command runs, by preparing the query semantic_view() would run for the member and reading its column type. So data_type follows the current source columns, and it is always the type a query returns for that member:

  • DuckDB type names. Types are spelled the way DuckDB spells them (VARCHAR, BIGINT, DECIMAL(38,2), VARCHAR[]), not as Snowflake would (TEXT, NUMBER(38,0)).

  • DuckDB’s own types. SUM over an integer column is a HUGEINT, and ENUM / STRUCT / MAP columns keep their types, both in what semantic_view() returns and in data_type.

  • When a member cannot be typed – its source table or column was dropped or renamed after CREATE – data_type is empty for that member, and the command still succeeds for the rest. Querying the member reports the underlying error. The same goes for a metric that cannot be queried on its own because it would aggregate across a fan-out (see How to Diagnose and Fix Fan Traps): its data_type is empty, and querying it reports the fan trap.

  • Over a temporary table, or a table changed in your open transaction, members are typed as semantic_view() reads them there: the command runs in your session and transaction.

  • PRIVATE members are typed too, though they cannot be queried directly.

  • Members that read a variable are typed with the variable taken as its default, or as a NULL of its type when it has no default, so no value is needed. SUM(amount * multiplier) with a DOUBLE multiplier is DOUBLE. See VARIABLES.

  • Role-played members – on a table that several relationships point at – are typed too, although querying one needs a metric whose USING names the role. The type is the same whichever role is used.

The cross-view forms (SHOW SEMANTIC METRICS without IN, or with IN SCHEMA) type the members of every view, so they take longer as the catalog grows: roughly a millisecond or two per view.

Metric Grain

Changed in version 0.12.0.

Like Snowflake, each metric is computed at the grain of its own logical table. When a query’s metrics sit at different grains – a metric on a parent table alongside one on the base table, two metrics on different child tables, or a single derived metric fusing two grains – each is aggregated separately over its own table and the results are joined on the queried dimensions. A metric on a parent table is therefore not multiplied by the number of child rows, and a parent row with no children is not dropped.

Before v0.12.0 the generated SQL was always anchored FROM <base table>, so these queries were rejected with a fan trap error rather than silently inflated.

Changed in version 0.14.0: This now holds for a query with a single grain too. A metric on a child of the base table, queried on its own, is aggregated from the child table and joined out to the dimensions, as in Snowflake – it used to be computed over FROM <base table> LEFT JOIN <child>. So a child row whose foreign key is NULL is counted under a NULL dimension value, a parent with no children has no row (or a NULL child metric when a parent-table metric brings its row in), and a metric’s value never depends on what else is queried. See Orphan Rows and Parents With No Children. Only a query whose metrics all live on the base table is still a single base-anchored SELECT; there the base table is every metric’s own table, so the two coincide.

Four boundaries apply:

  • A dimension below a metric’s grain (SUM(customers.balance) grouped by an order-grain dimension) is rejected in both systems. Snowflake’s rule is that the logical table for the dimension must be related to the logical table for the metric and must have “an equal or lower level of granularity than the logical table for the metric”; the extension’s fan trap detected error enforces the same condition. Per-grain aggregation does not make these answerable – the metric’s rows genuinely fan across the dimension’s values, so there is no single correct value per group.

  • A window metric whose inner aggregate lives on a non-base table is computed at its own grain – the inner aggregation is anchored at that table (a parent or a child of the base table), so it is neither inflated nor thinned by a base-table join. Window metrics whose inner aggregates sit at different grains still error, as those grains would need joining before the window runs. The exception is a pair joined one-to-one, where nothing fans out: it is answered from the base table instead (see What Is Still Refused, and Why).

  • An active semi-additive metric snapshots at its own grain, whether it is the query’s only grain or one of several: the RANK() runs over the metric’s own table rather than a base-anchored join, so the winning snapshot row is not added once per base-table row. A NON ADDITIVE BY dimension declared on another logical table is joined in so its ordering still binds. This matches Snowflake: probed directly, it returns each metric at its own grain, with the snapshot selection happening inside the semi-additive metric’s own-grain aggregation rather than over the joined row set. Snowflake also accepts a NON ADDITIVE BY dimension declared on another logical table, provided the reference is qualified (NON ADDITIVE BY (s.report_date)); the bare form resolves only within the metric’s own table. Both forms are accepted here too.

  • Multi-grain queries reaching a role-played table are computed when every metric’s USING names a role: each grain CTE joins its metrics’ relationship under its scoped alias and groups by the dimension bound to it, as the single-grain path already did. A metric without USING makes the query ambiguous, as in Snowflake, since a grain CTE would otherwise choose among the relationship instances by declaration order. This covers only a queried dimension’s own table – a WHERE member on a role-played table, a metric aggregated at one, or a table reachable only through one still error. A definition that merely declares role-playing does not lose per-grain emission: the test is what the query reaches, so unrelated grains in the same view are computed normally. The one pairing that still declines is a role-played dimension queried together with an active semi-additive metric: a snapshot group cannot carry a role, so the two grains would bind different instances of the same dimension and the outer join would compare them.

USING RELATIONSHIPS

Both systems support USING on metrics to select which relationship path a metric traverses. The syntax is identical:

METRICS (
    f.departures USING (dep_airport) AS COUNT(*)
)

Added in version 0.18.0: Snowflake’s multi-path behaviour: metrics with different USING paths to one table can be queried together with a dimension on it. Each metric is grouped through its own path and the results are merged on the queried dimensions, so a group one path never reaches shows NULL for that path’s metrics. A derived metric over such metrics is computed from the merged components. Snowflake’s documented form, f.departures USING (dep_airport) AS f.flight_count over a plain f.flight_count AS COUNT(*), works too: a metric can name other metrics on its own table.

Both systems refuse a dimension on a role-played table when a metric in the query names no path to it. Both also accept a dimension beyond the role-played table (regions reached from airports), taking the path from the metric’s USING and following the single relationship onward.

Facts Query Mode

Added in version 0.6.0.

Both systems allow facts to be queried directly as row-level columns. In Snowflake, facts appear in the SEMANTIC_VIEW() query function. In DuckDB Semantic Views, use the facts parameter:

-- DuckDB: query facts as row-level columns
SELECT * FROM SEMANTIC_VIEW(analytics
    FACTS region, net_price
);

Warning

In both systems, facts and metrics cannot be combined in the same query. Use FACTS or METRICS, not both.

Changed in version 0.18.0: Both systems now read a member in the clause it is listed in, following Snowflake’s 2026_07 behaviour change. A dimension listed in FACTS is read row by row, a fact listed in DIMENSIONS is grouped on, and FACTS with DIMENSIONS in one query is refused (see Cannot query facts with dimensions).

Semi-Additive and Window Metrics

Added in version 0.6.0.

Both systems support semi-additive metrics (NON ADDITIVE BY) and window function metrics (OVER with PARTITION BY EXCLUDING). The syntax is aligned:

-- Semi-additive: last balance per account, summed across customers
METRICS (
    a.balance NON ADDITIVE BY (date_dim) AS SUM(a.amount)
)

-- Window: rolling average excluding region from partition
METRICS (
    o.avg_qty AS AVG(total_qty) OVER (PARTITION BY EXCLUDING region ORDER BY month)
)

Like Snowflake, the default (ascending) direction selects the latest snapshot and DESC selects the earliest (see How to Use Semi-Additive Metrics).

The behavioral differences are:

  • NON ADDITIVE BY dimensions must be declared in the view’s DIMENSIONS clause. Snowflake validates against its own catalog.

  • Window metrics and NON ADDITIVE BY cannot be combined on the same metric (mutually exclusive).

  • NULL keys in a non-additive dimension: the default NULLS placement follows the direction (ASC → NULLS LAST, DESC → NULLS FIRST), matching DuckDB/Snowflake. Under NULLS LAST a NULL key never wins (the latest/earliest real snapshot is selected); under NULLS FIRST a NULL key wins. Add an explicit NULLS LAST to exclude NULL keys regardless of direction.

  • Window metrics cannot be mixed with aggregate metrics in the same query.

Materializations

Added in version 0.7.0.

Snowflake’s CREATE SEMANTIC VIEW SQL DDL does not include a materializations or pre-aggregation concept. Pre-aggregation in Snowflake is handled through separate materialized views.

DuckDB Semantic Views introduces a MATERIALIZATIONS clause that declares mappings from pre-aggregated tables to the dimensions and metrics they cover. When a query exactly matches a materialization, the extension routes to the pre-aggregated table instead of expanding raw sources. See How to Use Materializations for details.

Transactional DDL

Added in version 0.8.0.

This is a divergence. In Snowflake, each DDL statement executes as a separate transaction: a DDL statement issued inside an open transaction implicitly commits that transaction first, then runs and commits on its own. A CREATE, ALTER or DROP SEMANTIC VIEW therefore cannot be rolled back there.

DuckDB Semantic Views runs these statements inside the caller’s transaction, the way DuckDB runs its own DDL. BEGIN ... ROLLBACK discards an uncommitted CREATE, ALTER or DROP SEMANTIC VIEW, and a COMMIT publishes the DDL together with any data changes in the same transaction.

Some DuckDB-specific details to know before you build on it:

  • Reads inside an open transaction, DESCRIBE SEMANTIC VIEW and the SHOW SEMANTIC ... family included, see that transaction’s uncommitted CREATE, ALTER and DROP. Other connections see them once you commit.

  • CREATE SEMANTIC VIEW IF NOT EXISTS cannot fully absorb a race between two connections both running it against the same database at the same moment – one will succeed and the other will see a constraint or commit-conflict error. On a single connection, IF NOT EXISTS is reliable.

  • The non-IF EXISTS DROP and ALTER forms raise semantic view '<name>' does not exist when the view is absent at check time, instead of silently no-opping. The existence check and the write are atomic only inside an explicit transaction; under autocommit a drop committed by another writer in the window between them is not detected. Wrap the DDL in BEGIN ... COMMIT when you need the check to be reliable under concurrency.

See Transactional DDL and Known Limitations for the full mechanism and worked examples.

Schema Scoping and Name Resolution

Both systems scope a semantic view to a schema, so analytics.sales and staging.sales are two different views and a <schema>. qualifier on CREATE / DROP / ALTER decides which one a statement means.

Where they differ is how an unqualified reference is resolved. Snowflake uses the session’s current database and schema; DuckDB Semantic Views uses DuckDB’s own rule, the session search_path – the first schema on it holding a view of that name wins, so SET search_path = 'staging' makes a bare sales mean staging.sales. A view that is the only one of its name resolves whether or not its schema is on the path.

A name that exists only in schemas off the path is a miss. The error names the schemas it does live in and the path that was searched, rather than reporting a bare “does not exist” for a view SHOW SEMANTIC VIEWS plainly lists.

Two DuckDB-specific consequences:

  • Identifiers are case-insensitive on both sides of the dot, quoted or not, following DuckDB rather than Snowflake – analytics.sales, ANALYTICS.SALES and "Analytics"."Sales" all name the same view. Snowflake would treat the quoted spellings as case-sensitive. See CREATE SEMANTIC VIEW for the full identifier rule.

  • One catalog per database. A <database>. prefix that names a database other than the session’s is rejected: the extension manages a single catalog, in the database it was loaded into.

Feature Parity Notes

Snowflake CREATE SEMANTIC VIEW features that are commonly asked about, and where each one stands. Rows marked Supported have since landed; the rest are unimplemented, out of scope, or not planned, with the reason given:

Snowflake Feature

Status

Standard SQL over the view by name – SELECT region, AGG(revenue) FROM v GROUP BY region, with each metric passed to AGG

Supported, with Snowflake’s rules: GROUP BY expressions are the dimensions, MIN / MAX / ANY_VALUE of a metric mean AGG, other aggregates are ad-hoc metrics, WHERE filters before aggregation and HAVING after it, and SELECT *, joins, window functions, QUALIFY and qualified correlated references in the block that reads the view are refused. The query turns into the SEMANTIC_VIEW(...) construct, so its results and checks are the construct’s. A name that is also a table, a view or a CTE in scope reads that, following DuckDB’s name resolution. See How to Query a Semantic View with Standard SQL.

Pre-aggregation WHERE – SEMANTIC_VIEW( v METRICS ... DIMENSIONS ... WHERE <predicate> ), where the predicate may refer only to dimensions, facts, and expressions over them and “is applied before the metrics are computed”

Supported, written the same way in the SEMANTIC_VIEW(…) construct, and as the where_clause named parameter of semantic_view() (where is a reserved word in DuckDB’s named-parameter position, so the parameter cannot be spelled where :=). A prepared-statement parameter in the construct’s WHERE is one of the statement’s parameters. The predicate names declared dimensions and facts, is substituted to their expressions, and is applied before aggregation on every emission path – before the GROUP BY when the query is a single SELECT (one grain, dimensions only, or facts), inside each per-grain aggregate for multi-grain queries, inside the snapshot selection before the RANK for semi-additive metrics, and inside the inner aggregation before the window function. So “revenue for orders shipped after X” recomputes each group over the matching rows. Referencing a metric is rejected, matching Snowflake, and members the predicate names participate in the same reachability and fan-out checks as queried dimensions.

Query-time scalar expressions – SEMANTIC_VIEW( v DIMENSIONS DATE_PART('year', orders.order_date) AS year ), where a DIMENSIONS / FACTS item may be a scalar expression over dimensions or facts of the same logical table, a METRICS item may be an expression over metrics, and any item may take AS <alias>

Supported. An item may be an expression, in the construct and in a semantic_view() list ('DATE_PART(''year'', o.order_date) AS year'), with Snowflake’s rules: a DIMENSIONS or FACTS expression uses dimensions and facts of one table, and a METRICS expression uses metrics and aggregates of the dimensions and facts of one table. An expression item is planned as a member of its table, so COUNT(DISTINCT ...), ratios and per-grain metrics come out right, and the fan-trap checks apply to it. Any item may take AS <alias> (optional in the construct, as in Snowflake). See Expression Items.

Named filters – LABELS = (FILTER) on a fact or dimension resolving to BOOLEAN, referenced bare in a query’s WHERE (Snowflake GA May 2026)

Supported. LABELS = (FILTER) is accepted on a fact or dimension entry, between the name and AS as Snowflake writes it, or after the expression in any order among COMMENT / WITH SYNONYMS. It survives GET_DDL (written before AS), YAML export, and DESCRIBE (as a LABELS property row valued ["FILTER"]), and shows in the labels column of SHOW SEMANTIC DIMENSIONS / FACTS, as in Snowflake. Referencing the member bare in the query’s WHERE works via the pre-aggregation predicate above – a filter is an ordinary boolean member, so the label declares intent and drives introspection rather than gating resolution. The BOOLEAN requirement is enforced by DuckDB’s binder at query time, not at CREATE: CREATE binds the member but does not check its type, so a non-boolean filter raises DuckDB’s own type error when queried. Labels other than FILTER are rejected rather than silently dropped.

Tags – [WITH] TAG (<tag_name> = '<value>', ...) on the view, a logical table, a fact, a dimension or a metric

Supported, with Snowflake’s placement and syntax. Tags are stored with the view and shown as a TAGS row (a JSON object) in DESCRIBE; they survive GET_DDL and YAML export, and SHOW commands omit them, as Snowflake’s do. ALTER SEMANTIC VIEW ... SET TAG / UNSET TAG change the view’s own tags, at view level only, as in Snowflake. Snowflake requires each tag to exist as a CREATE TAG object; DuckDB has no tag objects, so the name (up to database.schema.tag) is stored as written. Values are limited to 256 characters, as in Snowflake. See Tags.

Cortex Analyst metadata – SAMPLE_VALUES and WITH CORTEX SEARCH SERVICE on members, IS_ENUM on dimensions, and MAX_STALENESS, AI_SQL_GENERATION, AI_QUESTION_CATEGORIZATION, AI_VERIFIED_QUERIES and COPY GRANTS on the view

Accepted and stored, with Snowflake’s syntax, so Snowflake DDL pastes as written. None of it changes a query: there is no Cortex Analyst to read it, no search service to call, and no automatic refresh for MAX_STALENESS to bound. The clauses show in DESCRIBE with Snowflake’s CUSTOM_INSTRUCTIONS and AI_VERIFIED_QUERY rows and survive GET_DDL and YAML export. MAX_STALENESS takes whole seconds, as Snowflake’s CREATE reference writes it, and also the '1 hour' form its materializations guide uses; ALTER ... SET / UNSET MAX_STALENESS work. COPY GRANTS is accepted and ignored, as DuckDB has no grants on these views. Snowflake’s GET_DDL can write sample values set outside the DDL as a trailing WITH EXTENSION (CA = '<json>') clause. That clause is not part of Snowflake’s CREATE syntax and its JSON is undocumented, so it is not planned: it is refused with an error that names it, and the values can be restated as SAMPLE_VALUES. See Cortex Analyst metadata.

View variables – VARIABLES (min_amount INTEGER DEFAULT 1000, ...) in CREATE, read by bare name in member expressions, and set per query with VARIABLES min_amount = 2000 as the last clause of SEMANTIC_VIEW(...)

Supported, with Snowflake’s rules: an entry is name [type] [{DEFAULT | :=} <expr>] [COMMENT '...'], an unqualified name is the variable and a qualified one the column, a default is evaluated by each query, and a variable with no default must be set by every query of the view. Members, the query’s WHERE and expression items read variables, and a window frame bound cannot. semantic_view() takes the values as variables := {'min_amount': 2000}, and a value can be a prepared-statement parameter. DuckDB names the refused types differently: a variable cannot be a list, array, STRUCT, MAP, UNION, VARIANT or JSON, the counterparts of Snowflake’s VARIANT, OBJECT and ARRAY. A standard-SQL query over the view cannot set variables, so defaults apply. A materialization covering a member that reads a variable is not used for routing. DESCRIBE shows VARIABLE rows, and GET_DDL and the YAML export write the variables back. See VARIABLES and Variables.

Column-level security

Out of scope; DuckDB handles access control

ASOF / temporal relationships

Not planned; standard equi-joins cover most use cases

CREATE OR ALTER, LABELS values other than FILTER, and the COPILOT clauses

Not planned; these are Snowflake-catalog or Cortex-specific and have no DuckDB equivalent

A Note on Snowflake’s YAML Spec

Snowflake’s YAML-based semantic view definition (CREATE SEMANTIC VIEW FROM YAML) is a separate interface designed for Cortex Analyst, Snowflake’s AI SQL generation layer. The YAML spec includes concepts that do not exist in the SQL DDL:

  • time_dimensions with granularity controls (the SQL DDL uses regular dimensions with date_trunc())

DuckDB Semantic Views supports YAML definition import (FROM YAML) and export (READ_YAML_FROM_SEMANTIC_VIEW()), but these use the extension’s own YAML schema – not Snowflake’s Cortex Analyst YAML spec. Where the SQL DDL carries Cortex Analyst metadata, the YAML uses Snowflake’s field names for it (sample_values, is_enum, cortex_search_service, max_staleness, module_custom_instructions, verified_queries); see YAML Definition Format. The DuckDB YAML format is a serialization of the same model used by the SQL DDL (tables, relationships, variables, facts, dimensions, metrics, materializations). It is designed for version control, migration, and sharing – not for AI prompt tuning. Comparisons against Snowflake YAML-spec-only features remain not applicable.

See How to Import and Export YAML Definitions for the DuckDB YAML workflow.