In LookML, how do dimensions and measures differ in the SQL Looker generates?
answer
- think about which SQL clause each reaches
- one of them lands in GROUP BY too
- the other is always wrapped in an aggregate
- references only work in one direction
basics
~10 sA LookML dimension becomes a non-aggregated expression that lands in SELECT and GROUP BY. A measure becomes an aggregate such as SUM or COUNT. Measures may reference dimensions; dimensions may never reference measures.
solid answer
~40 sWhen Looker builds a query, every selected `dimension` contributes its `sql:` expression to the `SELECT` list **and** to `GROUP BY`, because dimensions are row-level and define the grain of the result. Every selected `measure` contributes an aggregate — `SUM`, `COUNT`, `COUNT(DISTINCT …)`, `AVG` — computed over the rows in each of those groups. That asymmetry drives the reference rules: a measure's `sql:` can reference a dimension with `${field_name}`, because the aggregate is applied on top of a row-level expression, but a dimension can never reference a measure, since a row-level expression cannot contain a value that only exists per group. `${TABLE}.column` refers to the current view's aliased table; `${other_view.field}` refers to another view and forces Looker to add that join.
code
lookml · 15 linesdimension: status {
type: string
sql: ${TABLE}.status ;;
}
measure: total_amount {
type: sum
sql: ${TABLE}.amount ;;
}
measure: completed_revenue {
type: sum
sql: ${TABLE}.amount ;;
filters: [status: "complete"]
}go deeper
Recall that dimensions are row-level fields and measures are aggregates, and that a typed measure such as type: sum takes the bare column expression rather than a written-out SUM().
Explain the generated SQL: dimensions in SELECT and GROUP BY, measures as aggregates over each group, and why adding a dimension changes every measure value in the result.
Demonstrate judgment on ratio measures, filtered measures, and cross-view references that quietly add joins, plus how field design affects query cost against the warehouse.
Frame field design as an interface contract: how many measures to expose, whether business rules belong in LookML or upstream, and how naming and value formatting reduce misuse across teams.
## The distinction is a SQL distinction LookML fields are not two flavours of the same thing. A dimension and a measure land in different clauses of the SQL Looker writes, and that is the whole story. Take a small view: ``` view: orders { sql_table_name: public.orders ;; dimension: id { primary_key: yes type: number sql: ${TABLE}.id ;; } dimension: status { type: string sql: ${TABLE}.status ;; } measure: count { type: count } measure: total_amount { type: sum sql: ${TABLE}.amount ;; } } ``` Selecting `status`, `count` and `total_amount` produces roughly: ```sql SELECT orders.status AS status, COUNT(*) AS count, SUM(orders.amount) AS total_amount FROM public.orders AS orders GROUP BY 1 ``` The dimension appears twice — projected and grouped. The measures appear once each, wrapped in an aggregate. Add a second dimension and the grain gets finer: more groups, and every measure re-evaluates within each of them. ## Dimensions: row-level expressions A dimension's `sql:` must be valid at the level of a single row. `${TABLE}` is the placeholder Looker substitutes with the alias it gave this view's table in the current query, which is why you write `${TABLE}.status` rather than hard-coding the table name — the same view may be joined under different aliases. Dimensions carry a `type:` that tells Looker how to render and filter them: `string`, `number`, `yesno`, `date`, and the special `dimension_group` with `type: time`, which expands one timestamp column into a family of fields (raw, date, week, month, quarter, year) selected via `timeframes:`. A dimension can compute: `sql: ${TABLE}.first_name || ' ' || ${TABLE}.last_name ;;` is a perfectly ordinary dimension, and so is a `CASE` expression bucketing a numeric column. ## Measures: aggregates over the group A measure's `type:` selects the aggregate. `type: count` counts rows and takes no `sql:` parameter at all — Looker writes the count itself, which matters because it lets Looker substitute `COUNT(DISTINCT primary_key)` when a join has fanned the rows out. `type: sum`, `type: average`, `type: min`, `type: max` and `type: count_distinct` take a `sql:` parameter holding the *unaggregated* expression to aggregate: you write `sql: ${TABLE}.amount ;;`, not `sql: SUM(${TABLE}.amount) ;;`. `type: number` is the escape hatch for a computed measure, and it is where hand-written aggregation is allowed: ``` measure: average_order_value { type: number sql: 1.0 * ${total_amount} / NULLIF(${count}, 0) ;; value_format_name: usd } ``` Here `${total_amount}` and `${count}` reference other measures, so the expression is a ratio of two aggregates — the correct way to build a rate, because computing it row-by-row and averaging would give a different (usually wrong) number. Measures also accept a `filters:` parameter, which restricts the rows an individual measure aggregates without filtering the whole query: ``` measure: completed_revenue { type: sum sql: ${TABLE}.amount ;; filters: [status: "complete"] } ``` That compiles to a conditional aggregate, letting one row of results carry both filtered and unfiltered totals. ## The reference rules follow from the clauses - A measure may reference a dimension: the aggregate wraps a row-level expression. Legal and common. - A measure may reference another measure, as in the ratio above. - A dimension may **not** reference a measure. There is no row-level meaning for a value that only exists per group, and Looker rejects it. - `${other_view.field}` in either kind of field pulls another view into the query, so Looker adds that join. This is easy to do accidentally and turns a single-table query into a joined one, which changes both performance and — if the join fans out — the numbers. ## Why interviewers ask it Because the mistake it prevents is expensive. Candidates who think of a measure as "a number field" write `type: number` measures containing hand-rolled `SUM()`, which sidesteps the protections Looker applies to typed aggregates across joins. Others expect a filter on a measure to behave like a filter on a dimension, and are surprised it constrains groups rather than rows. Knowing which clause each field type reaches is what makes the rest of LookML predictable.
- Why does a LookML measure with type: count take no sql parameter?Looker writes the count expression itself. That indirection is what lets it emit `COUNT(DISTINCT primary_key)` instead of `COUNT(*)` when a join has duplicated the base rows, keeping the count correct without the developer intervening.
- How would you build an average order value measure correctly in LookML?Use `type: number` and divide two existing measures: `sql: 1.0 * ${total_amount} / NULLIF(${count}, 0) ;;`. Averaging a per-row ratio instead gives an average of ratios, which is a different and usually wrong number.
- What happens when a dimension's sql references a field in another view?Writing `${other_view.field}` forces Looker to add that view's join to the query, even if the user selected no fields from it. It can silently turn a single-table scan into a joined query and, if the join fans out, change aggregate results.
saying these in an interview costs you the question
- Writes SUM() inside a type: sum measure's sql parameter
- Thinks a dimension can reference a measure
- Believes measures are just number-typed dimensions
- Assumes adding a dimension leaves measure values unchanged
- Hard-codes the table name instead of using the TABLE placeholder