skip to content

In LookML, what does Liquid templating let you change at query time?

level: middleimportance: nice to knowfreq 26%

answer

  1. some parameters are templated before they run
  2. values, filters and user attributes are available
  3. two moments: query build and result render
  4. varying the SQL costs you cache hits

basics

~20 s

Liquid is a templating language Looker evaluates while building a query or rendering results, so sql, html, link and label parameters can vary with a field's value, the current filter values, or the signed-in user's attributes.

solid answer

~40 s

Looker runs **Liquid** over certain LookML parameters before the SQL is sent or the results are rendered. In `sql:` it can branch the generated SQL — `{{ _user_attributes['schema_name'] }}` to point at a per-tenant schema, or `{% if %}` blocks that change an expression based on a filter. In `html:` and `link:` it shapes output — `{{ value }}` and `{{ rendered_value }}` for the cell's own value, `{{ _filters['orders.created_date'] }}` to build a drill URL carrying the current filter. Templated filters use `{% condition %} … {% endcondition %}` to push a user's filter inside a derived table's SQL. The cost is real: Liquid in `sql:` means different users generate different SQL, which fragments the query cache and makes debugging harder, so use it deliberately.

code

lookml · 11 lines
lookml
measure: margin_pct {
  type: number
  sql: ${margin} / NULLIF(${revenue}, 0) ;;
  value_format_name: percent_1
  html:
    {% if value < 0 %}
      <span style="color:#b00000">{{ rendered_value }}</span>
    {% else %}
      {{ rendered_value }}
    {% endif %} ;;
}

go deeper

for a junior

Recognise Liquid tags in LookML and know they are a templating step Looker runs, most commonly to colour or format a cell's value in an html parameter.

for a middle

Explain the two evaluation moments — query composition versus result rendering — and name the common variables for the cell value, current filters and user attributes.

for a senior

Weigh the costs: cache fragmentation from per-user SQL, harder debugging because the file's SQL is not what ran, and the injection risk of interpolating attribute values.

for a principal

Set the policy on where dynamic SQL is acceptable at all, especially in multi-tenant models, and how templating interacts with persistence, caching strategy and the actual security boundary.

## What Liquid is doing here Liquid is a general-purpose templating language, and Looker uses it as a pre-processing step over selected LookML parameters. Two different moments matter: - Liquid in `sql:` and related SQL-producing parameters is evaluated **when Looker composes the query**, before it reaches the database. The database never sees Liquid. - Liquid in `html:`, `link:` and `label:` is evaluated **when results are rendered**, per cell or per field. The syntax has two forms: `{{ ... }}` outputs a value, and `{% ... %}` is a tag — control flow such as `{% if %} … {% elsif %} … {% else %} … {% endif %}`. ## The variables you will actually use - `{{ value }}` — the raw value of the current cell, in `html:`. - `{{ rendered_value }}` — the same value after Looker applies `value_format`. Use it when you want the formatted string in your HTML. - `{{ _user_attributes['name'] }}` — the signed-in user's value for a user attribute. - `{{ _filters['view_name.field_name'] }}` — the filter expression the user currently has applied to a field. - `{{ link }}` — the default drill link for the current cell, useful when wrapping it in custom HTML. There are also `_model`, `_explore`, `_view` and `_field` objects exposing names, handy for building generic links. ## Typical uses **Conditional formatting.** In `html:`, branch on the cell value to colour it: ``` measure: margin_pct { type: number sql: ... ;; html: {% if value < 0 %} <span style="color:#b00">{{ rendered_value }}</span> {% else %} {{ rendered_value }} {% endif %} ;; } ``` **Dynamic links.** A `link:` whose `url:` carries the current row's value or the current filters, so drilling lands on the right filtered dashboard. **User-attribute-driven SQL.** Multi-tenant deployments sometimes select a schema or partition per tenant: `sql_table_name: {{ _user_attributes['tenant_schema'] }}.orders ;;`. This works, and it is also the highest-risk use — see below. **Templated filters in derived tables.** A derived table normally cannot see the user's filters, because it is a subquery evaluated first. `{% condition %}` fixes that: ``` view: recent_orders { derived_table: { sql: SELECT * FROM public.orders WHERE {% condition order_date %} orders.created_at {% endcondition %} ;; } filter: order_date { type: date } } ``` The user's date filter is compiled into the derived table's own `WHERE`, so the subquery scans less. That is a genuine performance technique, not just a convenience. **Dynamic labels.** `label:` can incorporate a parameter or user attribute so a field renames itself according to a chosen unit or currency. ## The costs **Cache fragmentation.** Looker's query cache is keyed on the generated SQL. Liquid that varies SQL per user or per filter value produces many distinct queries, so cache hits fall and warehouse load rises. Conditional formatting in `html:` has no such cost, because the SQL is unchanged — it is specifically `sql:` templating that fragments. **Debuggability.** The SQL a developer reads in the LookML file is not the SQL that ran. You have to view the generated SQL per user to know what executed, and a bug that only appears for users with one attribute value is easy to miss in review. **It is not a security boundary on its own.** Branching SQL on a user attribute controls what the query asks for, but anyone who can reach the connection outside the model — SQL Runner, a direct client — is unaffected. Treat it as convenience or performance shaping; use `access_filter`, and ultimately database-side controls, for restriction. **Injection surface.** A user attribute value is interpolated into SQL text. If attribute values are populated automatically from an upstream system, an unexpected value becomes SQL text. Constrain what can be written into attributes used this way, and prefer values you control (an id, an enum) over free text. **PDTs and Liquid do not mix freely.** A persisted derived table is built once for everyone, so per-user Liquid in a persisted table's SQL has no coherent meaning; Looker restricts what may be templated in persisted tables. A table that must vary by user stays ephemeral. ## Interview framing This is differentiator material, not a core screener: many competent Looker developers use Liquid only for conditional HTML formatting. The strong answer names the two evaluation moments, gives one `sql:` use and one `html:` use, and then volunteers the cache and debuggability costs without being prompted.

  • What is the difference between value and rendered_value in a LookML html parameter?
    `value` is the raw underlying value, useful for comparisons in `{% if %}` tags. `rendered_value` is the same value after Looker applies the field's `value_format`, which is what you normally want to print inside your HTML so formatting stays consistent.
  • Why does Liquid in a sql parameter hurt query caching?
    Looker's cache is keyed on the generated SQL. If the SQL differs per user or per filter value, each variant is a separate cache entry, so hit rates drop and the warehouse runs more queries than it would with a single static statement.
  • Why can't a persisted derived table use per-user Liquid in its SQL?
    A PDT is built once, under no particular user's identity, and shared by everyone. Per-user templating has no coherent meaning at build time, so Looker restricts what may be templated in a persisted table — a table that must vary per user has to stay ephemeral.

saying these in an interview costs you the question

  • Thinks the database evaluates the Liquid tags
  • Uses user-attribute SQL branching as the security control
  • Interpolates unvalidated attribute values into SQL text
  • Ignores the cache cost of per-user generated SQL
  • Expects per-user templating inside a persisted derived table

context