skip to content

In LookML, how do dimensions and measures differ in the SQL Looker generates?

level: middleimportance: should knowfreq 68%

answer

  1. think about which SQL clause each reaches
  2. one of them lands in GROUP BY too
  3. the other is always wrapped in an aggregate
  4. references only work in one direction

basics

~10 s

A 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 s

When 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 lines
lookml
dimension: 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

for a junior

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().

for a middle

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.

for a senior

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.

for a principal

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

context