Snowflake Semantic Views vs dbt Semantic Layer: Where Metrics Live
Quick answer: A Snowflake semantic view is a schema-level object created with CREATE SEMANTIC VIEW, so it picks up Snowflake RBAC, tags, cloning, replication and sharing, and any SQL client can query it through SEMANTIC_VIEW(). The dbt Semantic Layer keeps metric definitions in version-controlled YAML with tests and CI, portable across warehouses, but serves them only through its own APIs. On snowflake semantic views vs the dbt semantic layer, most teams should run both: author in dbt, deploy as a semantic view.
Last updated: August 2026
Snowflake semantic views stopped being a Cortex Analyst implementation detail the moment they became a real schema object. CREATE SEMANTIC VIEW produces something that sits next to your tables, takes grants and tags, clones with the schema, replicates, shares, and answers plain SQL through SEMANTIC_VIEW(). That is a different animal from a YAML file parked in a stage.
If you already run metrics in dbt, this lands awkwardly. You have a MetricFlow spec under version control, tested in CI, reviewed in pull requests, and now the warehouse offers a competing home for the same definitions with better governance and worse engineering ergonomics. The instinct is to pick a winner. That is usually wrong.
This is the practical comparison: what each layer does, the gotchas, the cost trap in materializations, and the hybrid most teams with an existing dbt metrics layer should run. Every syntax and status claim is checked against Snowflake and dbt docs as of August 2026.
What does CREATE SEMANTIC VIEW actually create?
A semantic view is a schema-level object holding logical tables, their relationships, and three kinds of expression. It stores no data, only definitions and a join graph that Snowflake compiles into SQL over the physical tables.
- TABLES. Logical tables aliased over physical tables or views, with
PRIMARY KEYorUNIQUEconstraints so Snowflake knows the grain. - RELATIONSHIPS. Named foreign-key style edges between logical tables: the join graph, which is why consumers never write joins.
- FACTS. Row-level expressions.
l_extendedprice * (1 - l_discount)is a fact, not a metric. - DIMENSIONS. Grouping attributes, raw columns or expressions like
YEAR(o_orderdate). - METRICS. Aggregations. What a business user asks for by name, and what you must wrap in
AGG()when you query the view by name in theFROMclause.
Here is Snowflake's own TPC-H example. Read it closely: it shows the thing people get wrong first time, a fact feeding a metric rather than an aggregate defined twice.
CREATE SEMANTIC VIEW tpch_rev_analysis
TABLES (
orders AS SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS
PRIMARY KEY (o_orderkey)
WITH SYNONYMS ('sales orders')
COMMENT = 'All orders table for the sales domain',
customers AS SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.CUSTOMER
PRIMARY KEY (c_custkey)
COMMENT = 'Main table for customer data',
line_items AS SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.LINEITEM
PRIMARY KEY (l_orderkey, l_linenumber)
COMMENT = 'Line items in orders'
)
RELATIONSHIPS (
orders_to_customers AS
orders (o_custkey) REFERENCES customers,
line_item_to_orders AS
line_items (l_orderkey) REFERENCES orders
)
FACTS (
line_items.line_item_id AS CONCAT(l_orderkey, '-', l_linenumber),
orders.count_line_items AS COUNT(line_items.line_item_id),
line_items.discounted_price AS l_extendedprice * (1 - l_discount)
COMMENT = 'Extended price after discount'
)
DIMENSIONS (
customers.customer_name AS customers.c_name
WITH SYNONYMS = ('customer name')
COMMENT = 'Name of the customer',
orders.order_date AS o_orderdate
COMMENT = 'Date when the order was placed',
orders.order_year AS YEAR(o_orderdate)
COMMENT = 'Year when the order was placed'
)
METRICS (
customers.customer_count AS COUNT(c_custkey)
COMMENT = 'Count of number of customers',
orders.order_average_value AS AVG(orders.o_totalprice)
COMMENT = 'Average order value across all orders',
orders.average_line_items_per_order AS AVG(orders.count_line_items)
COMMENT = 'Average number of line items per order'
)
COMMENT = 'Semantic view for revenue analysis';
Notice orders.count_line_items: declared in FACTS as a COUNT over the related line items table, then averaged in METRICS. That is the shape to copy. Snowflake's docs describe facts as row-level attributes that usually act as helper concepts for building dimensions and metrics, and metrics as the aggregation across rows. A fact may roll up from a finer-grained table, as this one does, but the aggregation a business user asks for belongs in METRICS, defined once.
Clause order matters in the DDL too: the docs require FACTS before DIMENSIONS. Forward references are allowed, so a fact can refer to one declared below it.
How do you query a semantic view without an AI?
This is what changes the build-versus-buy calculation. A semantic view is not a Cortex-only artifact: two documented query forms exist, both plain SQL any JDBC or ODBC client can send.
-- Form 1: the SEMANTIC_VIEW() construct in the FROM clause.
-- Clause order here is the output column order.
SELECT * FROM SEMANTIC_VIEW(
tpch_rev_analysis
DIMENSIONS orders.order_year
METRICS orders.order_average_value
)
ORDER BY order_year;
-- Aliases work, with or without the AS keyword.
SELECT * FROM SEMANTIC_VIEW(
tpch_rev_analysis
DIMENSIONS orders.order_year AS yr
METRICS orders.order_average_value avg_order
)
ORDER BY yr;
-- Form 2: name the semantic view directly in FROM.
-- Metrics must be wrapped in AGG(); dimensions must appear in GROUP BY.
SELECT order_year, AGG(order_average_value) AS avg_order
FROM tpch_rev_analysis
GROUP BY order_year
ORDER BY order_year;
The second form is rewritten into the first: GROUP BY expressions become the DIMENSIONS clause, anything ungrouped becomes FACTS, and metrics go through the special AGG function. Passing a dimension or fact to any ordinary aggregate gives you an ad-hoc metric, handy for exploration but a bypass of the governed definition.
The privilege model is the quiet headline. Per the docs, querying a semantic view needs SELECT on the view itself, not on the tables it uses. That makes it a real privilege boundary: hand an analyst role the view and nothing else, and the only numbers they can reach are the ones you modelled.
The friendlier name-in-FROM form has hard limits: no joins, no subqueries, no window function calls, no QUALIFY, and none of the FROM-clause extensions PIVOT, UNPIVOT, MATCH_RECOGNIZE or LATERAL. If your BI tool generates any of those, it needs SEMANTIC_VIEW() wrapped in a derived table instead.
What a semantic view gives you that dbt metrics do not
None of this is about SQL generation quality. It is that the object lives in the database, dragging the whole Snowflake governance surface with it.
- Native RBAC.
GRANT SELECT ON SEMANTIC VIEWandGRANT ... ON FUTURE SEMANTIC VIEWS IN SCHEMAboth work. Metric access is a role grant, not an application setting. - Tags at every level. The
TAGclause works on the view, on each logical table, and on individual facts, dimensions and metrics. Classification and certification tags reach the metric itself, not just the table under it. - Private facts and metrics. A
PRIVATEexpression can be used inside other definitions but not queried directly or referenced in aWHEREcondition. That is how you ship a clean metric without exposing its intermediate parts. - Sharing and replication. Cloned with their schema, promoted across accounts by replication, shared through private and organizational listings and the Marketplace.
- Object-level lineage. Semantic views are a supported object type in Snowflake data lineage, so you can see which tables feed a metric. The limit: docs state column lineage is not currently supported for semantic views.
- Native AI consumption. Cortex Analyst and Cortex Agents read the definition directly instead of being handed a spec over an API.
-- The semantic view is the privilege boundary. Analysts get SELECT on the -- view and nothing on ORDERS, CUSTOMER or LINEITEM. GRANT SELECT ON SEMANTIC VIEW tpch_rev_analysis TO ROLE analyst_role; -- Cortex Analyst needs REFERENCES as well as SELECT when the role -- does not own the view. GRANT REFERENCES, SELECT ON SEMANTIC VIEW tpch_rev_analysis TO ROLE my_analyst_role; -- Cover everything the modelling team ships into the schema from now on. GRANT REFERENCES, SELECT ON FUTURE SEMANTIC VIEWS IN SCHEMA my_schema TO ROLE my_analyst_role; -- ALTER only sets tags at the view level. ALTER SEMANTIC VIEW tpch_rev_analysis SET TAG governance.certification = 'gold';
One wrinkle worth knowing before you design a tagging standard: ALTER SEMANTIC VIEW only sets and unsets tags at the semantic view level. Tags on logical tables, facts, dimensions and metrics have to be set through CREATE SEMANTIC VIEW, and CREATE OR ALTER SEMANTIC VIEW preserves existing tags rather than changing them.
What dbt still owns that semantic views cannot
The temptation after that list is to declare dbt metrics redundant. Mistake. A semantic view is good at runtime; dbt is good before deployment.
- Version control and review. A metric change becomes a pull request with a diff, an approver and a revert path. Snowflake's own best-practice guidance says to keep the semantic view YAML or DDL in Git.
- Tests and CI. dbt tests run against the models the metrics sit on, before deployment. Semantic views have no equivalent; you would write that harness yourself.
- Portability. The dbt Semantic Layer runs on Snowflake, BigQuery, Databricks, Redshift, Postgres and Trino. A semantic view is a Snowflake object and always will be.
- An open spec. MetricFlow, which powers the dbt Semantic Layer, is distributed under the Apache 2.0 license. That matters if vendor exposure is a board-level concern.
- Delivery APIs. A GraphQL API, a JDBC API and a Python SDK, so tools that will never speak Snowflake SQL still get governed numbers.
- Project-wide lineage. The
refgraph covers sources, staging, marts and metrics in one picture, and dbt's Catalog adds column-level lineage on Enterprise plans, which semantic view lineage does not provide.
One commercial detail: defining and querying metrics with the dbt Semantic Layer requires a paid dbt platform plan (Starter, Enterprise or Enterprise+). MetricFlow is open source; the served layer is not.
Snowflake semantic views vs dbt Semantic Layer: side by side
| Capability | Snowflake semantic view | dbt Semantic Layer |
|---|---|---|
| Where the definition lives | Schema object in the database | YAML in a Git repository |
| Access control | Snowflake RBAC; SELECT on the view suffices | dbt platform permissions plus warehouse credentials |
| Query from plain SQL | Yes, via SEMANTIC_VIEW() or the view name | No, only via the Semantic Layer APIs |
| Tagging | TAG on view, tables, facts, dimensions, metrics | meta and tags in YAML, not warehouse tags |
| Lineage | Object-level only; no column lineage | Full ref graph across the project |
| Portability | Snowflake only | Snowflake, BigQuery, Databricks, Redshift, Postgres, Trino |
| External sharing | Listings, Marketplace, data sharing | Not a warehouse-level share |
| Pre-aggregation | Materializations (preview) | Handled upstream in dbt models |
| Tests in CI | Build it yourself | Native dbt tests |
| AI consumption | Cortex Analyst and Agents read it directly | Through the Semantic Layer APIs |
| Licensing | Included with Snowflake | Paid dbt plan required |
The gotchas that bite in the first hour
Four documented restrictions cause most of the early frustration. None are bugs; all surprise you once.
- FACTS and METRICS are mutually exclusive. You cannot specify both in the same
SEMANTIC_VIEWclause, and you cannot omit all three ofMETRICS,DIMENSIONSandFACTS. - Clause order determines output column order. Put
DIMENSIONSbeforeMETRICSand dimensions come out first; reverse them and so does the result set. Any BI tool binding by ordinal position will notice. - FACTS plus DIMENSIONS means one logical table. Every fact and dimension in the query, including anything in the
WHEREclause, must be defined in the same logical table. Only combine them when the dimensions uniquely determine the facts, otherwise results are non-deterministic. - Dimension granularity constrains the metric. The dimension's table must be related to the metric's table and have an equal or lower level of granularity. Order date cannot slice a customer-grain metric.
-- Snowflake's documented tpch_analysis view. This query FAILS because the -- dimension's table is finer-grained than the metric's table. SELECT * FROM SEMANTIC_VIEW ( tpch_analysis DIMENSIONS orders.order_date METRICS customer.customer_order_count ); -- 010234 (42601): SQL compilation error: -- Invalid dimension specified: The dimension entity 'ORDERS' must be related to and -- have an equal or lower level of granularity compared to the base metric or -- dimension entity 'CUSTOMER'. -- Ask Snowflake which dimensions are legal for that metric before you guess. SHOW SEMANTIC DIMENSIONS IN tpch_analysis FOR METRIC customer_order_count;
That last one is the most common question we field after a semantic view goes live. Run SHOW SEMANTIC DIMENSIONS ... FOR METRIC while you model, and put the resulting list in the metric's COMMENT so nobody rediscovers it later.
One more: in the name-in-FROM form, a qualified name like nation.name is read as the view name, not the logical table, so two logical tables exposing a dimension called name break it. Name dimensions uniquely from the start.
Materializations: real speedup, real cost trap
Snowflake documents materializing dimensions and metrics as an open preview feature available to all accounts. It pre-computes selected expressions so queries read stored results instead of rescanning base tables. Check its status before you build a plan around it.
-- MAX_STALENESS must exist before any materialization can be added. -- Minimum is 120 seconds. ALTER SEMANTIC VIEW revenue_analysis SET MAX_STALENESS = '1 hour'; ALTER SEMANTIC VIEW revenue_analysis ADD MATERIALIZATION revenue_by_year WAREHOUSE = transform_wh REFRESH_MODE = INCREMENTAL IMMUTABLE WHERE ( order_year < 2026 ) AS DIMENSIONS order_year, customer_name METRICS total_revenue, order_count;
Here is the trap, documented plainly enough that nobody should be caught by it: materializations only benefit queries executed as Semantic SQL, meaning the SEMANTIC_VIEW construct or standard SQL against the view. Cortex Analyst, Cortex Agents and Snowflake CoWork issue physical SQL directly against the underlying tables and get no benefit at all.
So if you bought semantic views mainly for natural-language querying, materializations burn refresh credits and speed up nothing your users touch. They pay off when BI tools hit the view directly.
- MAX_STALENESS is a prerequisite. Set it before adding any materialization. Minimum 120 seconds, and you cannot unset it while materializations exist. Snowflake's user guide writes the value as a duration literal such as
'1 hour'while the SQL reference documents it as an integer number of seconds, so check the reference page before you script it. - Too-aggressive staleness self-destructs. If background refreshes cannot keep up with
MAX_STALENESS, Snowflake suspends the materialization and queries silently revert to full compute. - Use IMMUTABLE WHERE. Snowflake strongly recommends it to limit refresh scope. Closed prior periods are the obvious candidate.
- Not everything can be materialized. Window function metrics, semi-additive metrics and metrics specifying relationships are excluded.
The hybrid: author in dbt, deploy as a semantic view
This does not have to be a fight, because the semantic view is a passthrough target. Snowflake Labs publishes a dbt_semantic_view package on dbt Hub adding a semantic_view materialization, so a semantic view is just another dbt model with a ref-aware definition.
# packages.yml - check hub.getdbt.com for the current version
packages:
- package: Snowflake-Labs/dbt_semantic_view
version: 1.0.5
-- models/semantic/rev_analysis.sql
{{ config(materialized='semantic_view') }}
TABLES(
orders AS {{ ref('fct_orders') }} PRIMARY KEY (order_id),
customers AS {{ ref('dim_customers') }} PRIMARY KEY (customer_id)
)
RELATIONSHIPS(
orders_to_customers AS orders (customer_id) REFERENCES customers
)
FACTS(
orders.net_amount AS gross_amount - discount_amount
)
DIMENSIONS(
orders.order_year AS YEAR(order_date),
customers.segment AS segment
)
METRICS(
orders.total_revenue AS SUM(orders.net_amount),
orders.order_count AS COUNT(orders.order_id)
)
COMMENT='Revenue metrics, generated from dbt models'
Snowflake's docs describe the package as a direct passthrough to Snowflake's SQL layer, so new semantic view syntax is usable immediately without waiting for a package release. The tradeoff is ownership: it is an Apache 2.0 community package from the Snowflake Labs organisation rather than part of the Snowflake product, and dbt Hub states plainly that dbt Labs does not certify any package. Treat it as a documented pattern, not a supported product.
That split matches how the two tools are built. dbt owns models, tests and the deployment pipeline. The semantic view owns runtime governance, grants, tagging, sharing and AI consumption. One source of truth in Git, one enforced boundary in the database.
For redeploys use CREATE OR ALTER SEMANTIC VIEW, not CREATE OR REPLACE. It preserves existing grants without COPY GRANTS, and leaves the object unchanged if the definition already matches. That matters the first time a CI pipeline runs at 3am and quietly drops every analyst grant.
Nobody wants to hand-write that DDL for 400 tables
This objection deserves a straight answer. The TPC-H example is 40 lines for three tables; a mid-size warehouse has hundreds. Hand-authoring across all of them is not a project anyone should scope.
Snowflake's answer is Semantic View Autopilot, an AI-assisted generator in Snowsight. Feed it a description, a data source, and context as example SQL, table metadata or an exported BI model. It validates the SQL, discards invalid queries, extracts tables and relationships for review, infers keys from cardinality, and adds valid queries as verified queries. Tableau ingestion is documented; Power BI file ingestion (.pbit or .pbix) is a preview feature.
But the honest scoping answer is a decision, not a tool. You do not model 400 tables. You model the ten to fifteen metrics executives argue about and the star they hang off: revenue recognition, active customers, churn, margin, pipeline. The rest stays as tables and views analysts query normally.
Scoped that way a first semantic view is two to three weeks, not a quarter: pick the metric set, confirm each logical table's grain, write the relationships, draft with Autopilot, then spend the rest on review and on the synonyms and comments that make natural-language querying work. Agreeing what "active customer" means is the slow part, and no tool removes it.
How to decide, in four rules
- On dbt, Snowflake only, want governed AI querying. Keep dbt as the authoring layer, generate semantic views from it, let Cortex Analyst read them. The majority case.
- On dbt, multiple warehouses. Keep the dbt Semantic Layer as the portable contract and treat semantic views as a Snowflake-side projection for AI and in-database RBAC. Do not migrate away from dbt for a feature you can have alongside it.
- No dbt, no metrics layer, Snowflake only. Start with semantic views directly. You get RBAC, tagging and AI consumption without a second platform, and can add dbt later for the CI story.
- Heavy BI workload, small metric set. Semantic views plus materializations, remembering you are optimising the SQL path and not the Cortex path. Measure on the queries your BI tool actually sends.
The framing that holds up across our projects: dbt is where metrics get written and reviewed, the semantic view is where they get enforced and served. Pick one and you rebuild the other half badly.
Related Articles
Frequently Asked Questions
Q: Do I need dbt metrics if I have Snowflake semantic views?
Not for serving metrics inside Snowflake, but you lose what dbt does before deployment: version control, pull request review, tests and CI. Snowflake's own guidance is to keep the semantic view YAML or DDL in Git and deploy it from a pipeline, which describes dbt. Keep dbt as the authoring layer.
Q: Can I query a Snowflake semantic view from Power BI or Tableau?
Any client that can send SQL can query one through SEMANTIC_VIEW(). Snowflake also documents SYSTEM$EXPORT_TDS_FROM_SEMANTIC_VIEW to produce a Tableau Data Source file, currently a preview feature, and lists integrations from Sigma, Omni, Honeydew, Hex and ThoughtSpot. For Power BI the documented direction runs the other way: Autopilot ingests a .pbit or .pbix to generate a view.
Q: Is the stage-hosted YAML semantic model file still supported for Cortex Analyst?
Yes, it still works, but Snowflake explicitly recommends semantic views instead because semantic models are YAML files in a stage and lack native database integration. Semantic views support derived metrics, private expressions, the privilege system, sharing and the metadata catalog. Existing YAML models can be converted with the SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAML stored procedure.
Q: Why does my semantic view query fail with an error about level of granularity?
Error 010234 means the dimension sits on a logical table that is finer-grained than the metric's table, or is not related to it. A dimension's table must be related to the metric's table and have an equal or lower level of granularity. Run SHOW SEMANTIC DIMENSIONS ... FOR METRIC to list the valid dimensions.
Q: Do semantic view materializations make Cortex Analyst faster?
No. Snowflake documents that materializations benefit queries executed as Semantic SQL, meaning the SEMANTIC_VIEW construct or standard SQL against the view. Cortex Analyst, Cortex Agents and Snowflake CoWork issue physical SQL directly against the underlying tables and bypass materializations entirely. If natural-language querying is your main workload, materializations cost credits and return nothing.
Q: Can I share a semantic view with another Snowflake account?
Yes. Semantic views are in Snowflake's list of shareable objects and can be shared through private listings, organizational listings and the Marketplace. Two details catch people out. First, the semantic view references underlying tables, so you also grant REFERENCES and SELECT on the view to the share and SELECT on each table it uses. Run DESCRIBE SEMANTIC VIEW to find them. Second, for cross-account replication you must replicate the referenced objects too, or the view will not function in the target account. Semantic views also clone when the schema containing them is cloned.
