How to Use FACTS for Reusable Row-Level Logic¶
This guide shows how to use the FACTS clause to define reusable row-level expressions that metrics can reference. FACTS eliminate duplicated calculations across metrics and support chaining (one fact referencing another). Facts can also be queried directly as row-level columns and annotated with comments, synonyms, and access modifiers.
Prerequisites:
A working semantic view with
TABLES,DIMENSIONS, andMETRICS(see Multi-Table Semantic Views, or Building a Complete Model for a guided introduction to facts)Understanding of aggregate vs. row-level expressions in SQL
Define a Basic Fact¶
A fact is a named row-level expression scoped to a table alias. Unlike metrics, facts do not contain aggregate functions. They compute a value for each row.
CREATE SEMANTIC VIEW sales AS
TABLES (
li AS line_items PRIMARY KEY (id)
)
FACTS (
li.net_price AS li.extended_price * (1 - li.discount)
)
DIMENSIONS (
li.region AS li.region
)
METRICS (
li.total_net AS SUM(li.net_price)
);
The metric total_net references the fact net_price. At expansion time, the extension inlines the fact expression into the metric: SUM(li.extended_price * (1 - li.discount)).
Chain Facts Together¶
Facts can reference other facts. The extension resolves them in dependency order (topological sort) and inlines them recursively.
CREATE SEMANTIC VIEW sales AS
TABLES (
li AS line_items PRIMARY KEY (id)
)
FACTS (
li.net_price AS li.extended_price * (1 - li.discount),
li.tax_amount AS li.net_price * li.tax_rate
)
DIMENSIONS (
li.region AS li.region
)
METRICS (
li.total_net AS SUM(li.net_price),
li.total_tax AS SUM(li.tax_amount)
);
Here tax_amount references net_price. The extension resolves the chain:
net_price=li.extended_price * (1 - li.discount)tax_amount=(li.extended_price * (1 - li.discount)) * li.tax_rate
Both metrics receive the fully inlined expressions.
Use Facts in Multi-Table Views¶
Facts are scoped to their table alias. In a multi-table view, each fact references columns from its own table.
CREATE SEMANTIC VIEW analytics AS
TABLES (
li AS line_items PRIMARY KEY (id),
o AS orders PRIMARY KEY (id),
c AS customers PRIMARY KEY (id)
)
RELATIONSHIPS (
li_to_order AS li(order_id) REFERENCES o,
order_to_customer AS o(customer_id) REFERENCES c
)
FACTS (
li.net_price AS li.extended_price * (1 - li.discount),
li.tax_amount AS li.net_price * li.tax_rate
)
DIMENSIONS (
o.region AS o.region,
c.country AS c.country
)
METRICS (
li.total_net AS SUM(li.net_price),
li.total_tax AS SUM(li.tax_amount)
);
The facts are still scoped to li (line_items), but the dimensions come from o (orders) and c (customers). The extension joins all necessary tables based on what the query requests.
Warning
An expression may only reference columns of its own table. A fact,
dimension, or metric expression that names a raw column of another logical
table – li.margin AS li.extended_price - c.discount – is rejected at
CREATE:
semantic view: metric 'margin' references 'c.discount', a column of table
'c', but a metric expression may only reference columns of its own table
('li'). To use a value from another table, define a FACT on that table and
reference the fact by name (e.g. 'c.<fact_name>').
Snowflake applies the same rule (“Expressions cannot refer to base table columns from other tables”) and rejects at definition time as well.
Changed in version 0.12.0: Previously the CREATE succeeded and the reference surfaced at query
time as a DuckDB unknown-alias error.
Referencing a named fact – on its own table or, given a relationship, on
another – is fully supported for facts, dimensions and metrics alike; see
Reference a Fact on Another Table below. Composing metrics across tables
(m AS t1.metric_a + t2.metric_b) is likewise supported. It is only the
raw foreign column that is rejected.
Reference a Fact on Another Table¶
Added in version 0.12.0.
A fact, dimension or metric expression may reference a declared fact – on its own table, or on another one given a relationship. This is Snowflake’s documented way to cross tables, and the only one: define the fact where its columns live, then refer to it from the connected table.
Changed in version 0.12.0: Dimension expressions reference facts on the same footing as metrics. Before, facts were inlined only into metric and fact expressions, so a fact named in a dimension was emitted verbatim and failed on the unknown column – including when the fact was declared on the dimension’s own table.
CREATE SEMANTIC VIEW order_margin AS
TABLES (
o AS orders PRIMARY KEY (id),
c AS customers PRIMARY KEY (id)
)
RELATIONSHIPS (
o_to_c AS o(customer_id) REFERENCES c(id)
)
FACTS (
c.cust_discount AS c.discount
)
METRICS (
o.net_total AS SUM(o.amount - c.cust_discount)
);
The fact’s expression is inlined at the reference site and its table is joined
for you, so net_total computes over orders LEFT JOIN customers. The
reference must be to the fact by name – bare (cust_discount) or
qualified by the fact’s own table (c.cust_discount). A qualifier naming any
other relation is read as a raw column of that relation, which is the
unsupported form in the warning above.
Warning
The fact’s table must not fan the member’s. Reaching a fact requires joining its table, and if that join multiplies the member’s rows – a fact on a child table, reached across a one-to-many edge – the aggregate would count each row once per child. There is no correct number to return, so the query is rejected:
semantic view 'v': fan trap detected -- 'bad_margin' (table 'o')
references the fact 'item_cost' on table 'li', and relationship 'li_to_o'
fans out on the way there, so joining it would multiply 'bad_margin's
rows. Reference a fact on a table reachable without fanning out, or
define the fact on 'o'.
Facts on the parent side (many-to-one, one row per member row) join safely and are the usual case – a customer’s discount, a product’s list price, a region’s tax rate.
Query Facts Directly¶
Added in version 0.6.0.
Facts can be queried as row-level columns using the facts parameter in semantic_view(). Unlike metric queries, fact queries return individual rows without aggregation.
SELECT * FROM semantic_view('analytics',
facts := ['net_price', 'tax_amount']
);
Each row in the result contains the computed fact values. Dimensions can be included alongside facts – they appear as columns but do not trigger GROUP BY:
SELECT * FROM semantic_view('analytics',
dimensions := ['region'],
facts := ['net_price']
);
Warning
Facts and metrics cannot be combined in the same query. Use facts := [...] OR metrics := [...], not both. Attempting to mix them produces an error.
For a complete guide to fact queries, including wildcard selection and troubleshooting, see How to Query Facts Directly.
Annotate Facts with Metadata¶
Added in version 0.6.0.
Facts accept four annotations: COMMENT, WITH SYNONYMS, the PRIVATE / PUBLIC access modifiers, and LABELS = (FILTER). They may appear in any order after the expression.
CREATE SEMANTIC VIEW sales AS
TABLES (
li AS line_items PRIMARY KEY (id)
)
FACTS (
li.net_price AS li.extended_price * (1 - li.discount) COMMENT = 'Price after discount',
PRIVATE li.raw_margin AS li.price - li.cost WITH SYNONYMS = ('margin', 'gross_margin')
)
DIMENSIONS (
li.region AS li.region
)
METRICS (
li.total_net AS SUM(li.net_price),
li.total_margin AS SUM(li.raw_margin),
profit_margin AS total_margin / total_net * 100
);
COMMENTadds a human-readable description, visible inDESCRIBE SEMANTIC VIEWandSHOW SEMANTIC FACTSoutput.WITH SYNONYMSadds informational alternative names for discoverability.PRIVATEprevents a fact from being queried directly viafacts := [...], while still allowing it to be referenced in metric expressions – here the base metrictotal_marginaggregates the private factraw_margin. Private facts are also excluded from wildcard expansion (alias.*).LABELS = (FILTER)marks a boolean fact as a reusable named filter for where_clause predicates. See Mark a Named Filter.
Note that profit_margin is a derived metric: it has no table alias and combines two base metrics by name. A derived metric may not contain an aggregate of its own, so the aggregation over raw_margin has to live in the base metric li.total_margin rather than inline in profit_margin.
For more on metadata annotations, see How to Use Metadata Annotations.
Verify the Inlined SQL¶
Use explain_semantic_view() to confirm that fact expressions are inlined correctly:
SELECT * FROM explain_semantic_view('analytics',
dimensions := ['region'],
metrics := ['total_net']
);
The expanded SQL shows the fully inlined expression in the SELECT clause, with no reference to the fact name.
Troubleshooting¶
- Circular fact references
Facts that reference each other in a cycle cause a define-time error. The extension detects cycles during
CREATE SEMANTIC VIEWand reports which facts are involved.- Aggregate functions in facts
Facts must be row-level expressions. Using an aggregate function like
SUM()orCOUNT()in a fact expression causes a define-time error. Aggregation belongs in theMETRICSclause.- Fact name not found
If a metric references a fact name that does not exist, the extension treats it as a regular column reference. If the column also does not exist, the query fails with a DuckDB column-not-found error. Double-check fact names match exactly.
- Private fact cannot be queried
Facts marked
PRIVATEcannot be queried viafacts := [...]. They return an error:fact '<name>' is private and cannot be queried directly. Remove thePRIVATEkeyword to make a fact queryable.