Skip to main content
On this page

Model Fields Measures

Pass through, evaluate or declare

Decide what readers of your model get when it reads a measure: the same measure, a fixed value, or a new measure.

Audience
Data teams
Prerequisites
  • A model you can edit
  • The basics of measures, on the Measures overview

When a model reads a measure from its source, how you write it decides what readers of the model get. A pass-through gives them the same measure. An EVAL gives them a value fixed at your model’s grouping. A MEASURE declaration gives them a new measure that you own. A tick in the field list writes a pass-through.

The three choices

These lines come from the Region Mix query:

workspaces/shop/query_contracts/region_mix.sql
SELECT
shop.region_group,
shop.sum__amount,
MEASURE(
'revenue_per_customer',
EVAL(shop.sum__amount) / NULLIF(EVAL(shop.count_distinct__customer_id), 0)
) AS "Revenue per Customer"
FROM workspace.shop AS shop
You writeReaders of your model getUse it when
shop.sum__amount, or *A pass-through: the same measure, with this model’s filters and joins added. A reader totals it again from the source rows, at the reader’s own grouping.Readers should group the measure their own way.
EVAL(shop.sum__amount)The measure’s value at this SELECT’s grouping. It is exported as a dimension, fixed at this grain.You need the number here, and nobody should total it again.
MEASURE('revenue_per_customer', EVAL(...) / NULLIF(EVAL(...), 0))A new measure with its own name. Readers work it out again at their grouping, from the totals it reads.The meaning changes, such as a ratio of two measures.
shop.region_group or EVAL(shop.region_group)The same dimension, with the same name.Always. A dimension has one form.
EVAL(a) / EVAL(b) AS ratio, with no declarationAn unnamed output. It works in this model’s own results and charts, but no other model can read it.Only for a value this model shows itself. The hint unnamed_output offers to name it.

An AS alias changes the label only. shop.sum__amount AS "Sales" is still the pass-through of Revenue, with the name sum__amount.

What readers get

These examples save two queries of the sample shop, then read each of them from a third query. A query reads another query of its workspace as FROM query.<name>.

Pass the measures through

The query orders_by_region passes Orders and Customers through, grouped by region:

workspaces/shop/query_contracts/orders_by_region.sql
SELECT
shop.region_group,
shop.region,
shop.count_distinct__order_id,
shop.count_distinct__customer_id
FROM workspace.shop AS shop
GROUP BY
shop.region_group,
shop.region
ORDER BY
shop.region_group,
shop.region
Region GroupRegionOrdersCustomers
AmericasAMER728232
EuropeEMEA777231
Rest of WorldAPAC671229
Rest of WorldLATAM760228
Rest of Worldnull6456

A reader with no grouping:

workspaces/shop/query_contracts/read_orders_by_region.sql
SELECT
q.count_distinct__order_id,
q.count_distinct__customer_id
FROM query.orders_by_region AS q
OrdersCustomers
3,000240

A reader grouped by region group:

workspaces/shop/query_contracts/orders_by_region_group.sql
SELECT
q.region_group,
q.count_distinct__order_id,
q.count_distinct__customer_id
FROM query.orders_by_region AS q
GROUP BY
q.region_group
ORDER BY
q.region_group
Region GroupOrdersCustomers
Americas728232
Europe777231
Rest of World1,495240

Each reader counts again from the order rows. Rest of World has 240 customers, not 229 + 228 + 56, because Kater counts each customer once.

Evaluate the measures

The query orders_by_region_fixed is the same query with EVAL around each measure:

workspaces/shop/query_contracts/orders_by_region_fixed.sql
SELECT
shop.region_group,
shop.region,
EVAL(shop.count_distinct__order_id),
EVAL(shop.count_distinct__customer_id)
FROM workspace.shop AS shop
GROUP BY
shop.region_group,
shop.region
ORDER BY
shop.region_group,
shop.region

Its own rows are the same as those of orders_by_region. But a reader now gets two dimensions, fixed at the region. A reader with no grouping gets the five fixed values:

workspaces/shop/query_contracts/read_orders_by_region_fixed.sql
SELECT
q.region,
q.count_distinct__order_id,
q.count_distinct__customer_id
FROM query.orders_by_region_fixed AS q
RegionOrdersCustomers
AMER728232
EMEA777231
APAC671229
LATAM760228
null6456

To get one total, the reader has to add the fixed values up:

workspaces/shop/query_contracts/orders_added_up.sql
SELECT
MEASURE('orders_added_up', KATER__SUM(q.count_distinct__order_id)) AS "Orders Added Up",
MEASURE('customers_added_up', KATER__SUM(q.count_distinct__customer_id)) AS "Customers Added Up"
FROM query.orders_by_region_fixed AS q
Orders Added UpCustomers Added Up
3,000976

Orders adds up to the right number by chance, because each order has one region. Customers adds up to 976, because a customer who ordered in four regions counts four times. The pass-through gave 240. Evaluate a measure only when its value at your grouping is the number readers need.

Declare a new measure

A MEASURE declaration over EVALs is a measure of its own. Readers work it out again at their grouping. See Measures built from measures for Revenue per Customer, which a reader grouped by currency works out again as $1,544.44.

When Kater writes EVAL for you

Kater writes EVAL around a measure where a pass-through cannot stand, and when you lock a measure in:

  • In HAVING, QUALIFY or ORDER BY. A condition or a sort reads the measure’s value at the model’s grouping.
  • Inside an expression. EVAL(shop.sum__quantity) / 1000 works the measure out at the model’s grouping, then divides. The result is a new value: an unnamed dimension until you declare it.
  • In a SELECT that has HAVING or QUALIFY. These clauses decide which rows count from other rows. So the SELECT cannot pass a measure through, and it evaluates each measure instead.
  • When you lock a measure in. In a query, Edit › Lock in on a measure turns its pass-through into EVAL.

Filter by this number

On a measure’s column in a query, Filters opens with Is In The Top and Is In The Bottom. The number conditions follow, such as Is Greater Than. This query shows Orders and Units by region:

workspaces/shop/query_contracts/orders_and_units_by_region.sql
SELECT
shop.region,
shop.count_distinct__order_id,
shop.sum__quantity
FROM workspace.shop AS shop
GROUP BY
shop.region

Filters › Is Greater Than on Orders, with 700, writes the condition in HAVING. In the same edit, it evaluates every measure the SELECT passes through, and keeps their names and labels:

workspaces/shop/query_contracts/orders_and_units_by_region.sql
SELECT
shop.region,
EVAL(shop.count_distinct__order_id),
EVAL(shop.sum__quantity)
FROM workspace.shop AS shop
GROUP BY
shop.region
HAVING
KATER__IS_GREATER_THAN(EVAL(shop.count_distinct__order_id), 700)
RegionOrdersUnits
AMER7282,179
LATAM7602,260
EMEA7772,421

APAC, with 671 orders, and the 64 orders with no region drop out. A filter on a measure applies to its own query only.

Top N

Filters › Is In The Top writes a rank condition in QUALIFY. The Top Regions query keeps the 5 region and currency rows with the most revenue:

workspaces/shop/query_contracts/top_regions.sql
SELECT
shop.region,
shop.currency,
EVAL(shop.sum__amount),
EVAL(shop.count_distinct__order_id)
FROM workspace.shop AS shop
GROUP BY
shop.region,
shop.currency
QUALIFY
KATER__IS_IN_TOP(EVAL(shop.sum__amount), 5)
ORDER BY
3 DESC
RegionCurrencyRevenueOrders
LATAMUSD$116,577.89760
AMERUSD$105,459.33728
APACUSD$102,849.35671
EMEAEUR€84,371.35532
EMEAUSD$36,129.01245

This ranks euros against dollars. To rank within each currency, partition the condition. OVER (PARTITION BY shop.currency) keeps the top region of each currency:

workspaces/shop/query_contracts/top_region_per_currency.sql
SELECT
shop.region,
shop.currency,
EVAL(shop.sum__amount),
EVAL(shop.count_distinct__order_id)
FROM workspace.shop AS shop
GROUP BY
shop.region,
shop.currency
QUALIFY
KATER__IS_IN_TOP(EVAL(shop.sum__amount), 1) OVER (PARTITION BY shop.currency)
ORDER BY
shop.currency
RegionCurrencyRevenueOrders
EMEAEUR€84,371.35532
LATAMUSD$116,577.89760

A partition keeps a measure that cannot be added up across currencies from being ranked across them.

Top N and Bottom N keep ties, so they can keep more rows than the count. From June to September 2026, July and September both had 137 orders. The bottom 2 months are three months:

workspaces/shop/query_contracts/bottom_months.sql
SELECT
shop.order_month,
EVAL(shop.count_distinct__order_id)
FROM workspace.shop AS shop
WHERE
shop.order_month >= DATE '2026-06-01'
GROUP BY
shop.order_month
QUALIFY
KATER__IS_IN_BOTTOM(EVAL(shop.count_distinct__order_id), 2)
ORDER BY
EVAL(shop.count_distinct__order_id) ASC NULLS LAST
Order MonthOrders
2026-06-01136
2026-07-01137
2026-09-01137

A query with no ORDER BY gets one from Top N or Bottom N, so its rows come out in rank order. A query that already sorts keeps its own sort.

A measure the model declares itself

HAVING, QUALIFY and ORDER BY can also read a measure that the same SELECT declares. They apply to the model’s results, so they read the measure at the model’s grouping:

workspaces/shop/query_contracts/large_orders_by_region.sql
SELECT
shop.region,
MEASURE('large_orders', KATER__COUNT_DISTINCT(CASE WHEN shop.order_size = 'large' THEN shop.order_id END)) AS "Large Orders"
FROM workspace.shop AS shop
GROUP BY
shop.region
HAVING
EVAL(large_orders) > 340
RegionLarge Orders
LATAM360
EMEA372

Anything else in the same condition must be one of the model’s outputs.

What a reader never sees

A model’s GROUP BY, ORDER BY, LIMIT, OFFSET and DISTINCT shape only its own run. The query top_two_regions keeps the two regions with the most orders:

workspaces/shop/query_contracts/top_two_regions.sql
SELECT
shop.region,
shop.count_distinct__order_id
FROM workspace.shop AS shop
GROUP BY
shop.region
ORDER BY
2 DESC
LIMIT 2
RegionOrders
EMEA777
LATAM760

A reader of it with no grouping:

workspaces/shop/query_contracts/read_top_two_regions.sql
SELECT
q.count_distinct__order_id
FROM query.top_two_regions AS q
Orders
3,000

The reader gets 3,000, not 777 + 760. A passed-through measure is totalled again from the source rows, so the LIMIT does not reach it. To hand readers only the top rows, use Top N. It evaluates the measure, so a reader of the Top Regions query gets its 5 rows and their fixed values.

Renaming a measure

A measure that is passed through keeps its name. * RENAME cannot give it a new one:

workspaces/shop/query_contracts/orders_renamed.sql
SELECT
* RENAME (count_distinct__order_id AS orders)
FROM workspace.shop AS shop

The compiler refuses it: “RENAME can’t give the measure count_distinct__order_id a new name, because a measure passed through keeps its name.” Declare the new name with MEASURE instead. This query is saved as orders_renamed:

workspaces/shop/query_contracts/orders_renamed.sql
SELECT
shop.region,
MEASURE('orders', EVAL(shop.count_distinct__order_id)) AS "Orders"
FROM workspace.shop AS shop
GROUP BY
shop.region
ORDER BY
shop.region

A reader of it with no grouping:

workspaces/shop/query_contracts/read_orders_renamed.sql
SELECT
q.orders
FROM query.orders_renamed AS q
Orders
3,000

orders is a new measure, so readers still work it out again from the order rows. To change only what people see, change the label: shop.count_distinct__order_id AS "Order Count" keeps the name and the pass-through.

Rules

  • A pass-through is the default. A tick in the field list, a bare name and * all pass the measure through. Readers can then group it any way.
  • EVAL fixes the value. Readers of an EVAL output get a dimension at your grouping. Adding it up again can give a wrong number.
  • A new measure needs a new name. MEASURE('count_distinct__order_id', EVAL(shop.count_distinct__order_id)) takes a name that is already in scope, so the compiler refuses it.
  • HAVING and QUALIFY show the value at your grouping. A measure beside either shows its value at this grouping, as EVAL does, and readers can’t add it up again. The compiler warns you to write EVAL, so the SQL says so.
  • A reader never sees your row shape. Your GROUP BY, ORDER BY, LIMIT, OFFSET and DISTINCT shape only your own run.
  • Rules carry through. A pass-through, and a measure built on another, keep its rules, such as one currency per row. See Measures you cannot add up.

Common errors

CodeCauseFix
field_name_in_scopeA declaration takes a name that is in scope, such as MEASURE('count_distinct__order_id', EVAL(shop.count_distinct__order_id)). The compiler says “count_distinct__order_id is already a measure in workspace.shop, which this model selects from”.Apply the fix Use shop.count_distinct__order_id, which writes the pass-through. Or give the new measure a new name.
measure_evaluated_beside_clauseWarning. A measure in a SELECT that has HAVING or QUALIFY. The compiler says “shop.count_distinct__order_id in the SELECT shows its value at this grouping, because the query has HAVING”.Apply the fix Write EVAL(shop.count_distinct__order_id). A filter from the field menu writes it for you.
invalid_measure_rename* RENAME on a measure.Declare the new name with MEASURE('new_name', EVAL(...)), or change only the label.
invalid_measure_reaggregationA measure inside an aggregate, such as SUM(shop.sum__quantity).Select the measure on its own for its total.
unnamed_outputHint. A calculation with no declaration, which no other model can read.Apply the fix that names it as a measure or a dimension.