6 min read

Layer Your LookML: A Practitioner’s Guide to Refinements in Looker Core

You know the drill. You open a view file in a mature Looker Core project and scroll past two hundred dimensions to find the one field you need to change. Or the warehouse schema shifts, you regenerate the LookML, and suddenly your hand-written business logic is either gone or duplicated across three files. Neither is a bug in Looker. It's a symptom of code that doesn't separate what came from the database from what your team wrote on top of it.

LookML refinements fix that. They let you add dimensions, measures, joins, and explore logic to an existing view or explore without touching the base file. The syntax is a + prefix on the object name. Combined with a disciplined include order, refinements give you a layered architecture where the machine-generated code stays regenerable and your business logic stays yours.

At a Glance

  • LookML refinements use a + prefix to modify a view or explore without editing the base file
  • Layer your code into four zones: model, base, standard, and logical layers
  • Regenerable code lives in _base/; business logic lives in layers/
  • Later includes win on conflicting parameters; joins and other additive parameters accumulate
  • Refinements are the cleanest way to customize blocks and cross-project imports you can't edit directly

A Looker engineer laid out this layered approach in a 2020 forum post. Google documents the same pattern today in the LookML refinements guide. The feature has been GA since Looker 7.6. If your team has been avoiding refinements because they looked like just another syntax option, this guide is the practitioner's case for using them as your default project structure.

 

Refinements in 60 seconds

A refinement targets an existing view or explore by prefixing its name with +. Looker merges the refinement into the base object using the same rules as extends, but it does not create a new named copy. You end up with one view, assembled from multiple files.

view: fact {
  sql_table_name: fact ;;
}

view: +fact {
  label: "Facts"
  dimension: date { type: date }
}

view: +fact {
  label: "Quantified Facts"
  dimension: amount { type: number }
}

These three declarations combine into one view:

view: fact {
  sql_table_name: fact ;;
  label: "Quantified Facts"
  dimension: date { type: date }
  dimension: amount { type: number }
}

When the same parameter appears in multiple refinements, the last one wins. Within a single file, the lowest line number loses. Across files, the last included in your model wins.

Some parameters are additive instead of overriding. Joins on explores, filters, link on dimensions, and several others accumulate rather than replace. Check the additive parameters list before assuming a refinement will overwrite the base.

 

Refinements vs extends

Both mechanisms let you build on existing LookML. The difference is what they produce.

Refinements vs. Extends

  Refinements view: +orders Extends extends: [orders_base]
Creates a new object? No, modifies in place Yes, new named copy
Base file editable? No, base stays untouched Base stays untouched
Best for Enriching shared or generated views Variants that need different names
PDTs Safe, no duplicate tables Avoid, each extend copies the PDT

Use refinements when you want to add fields, labels, or joins to an object that already exists in your project. Use extends when you need a second view with a different name that inherits from a base but diverges.

For most enrichment work, refinements are simpler. Google's docs call them "a simpler and cleaner alternative to extends" for the majority of use cases.

 

The four layers

The layered model separates code by how safe it is to regenerate and by how closely it tracks business logic versus database schema. File names are flexible. The layer's purpose is what matters.

01

Model File

Orchestration only. Connection, includes, no business logic.

02

Base / Raw

Machine-generated, safe to regenerate. Never hand-edit.

03

Standard / Basic

Schema-shaped enrichment: PKs, labels, hides, basic joins.

04

Logical Layers

Business logic grouped by concern, not by Looker object.

1. Model file: orchestration only

Keep your model file thin. It declares the connection, includes your layer files in order, and optionally applies table-name overrides. No business logic here.

connection: "bigquery_prod"

include: "/_base/_raw.lkml"
include: "/_standard/*.lkml"
include: "/layers/*.lkml"
include: "/explores/*.lkml"

2. Base / raw: safe to regenerate

This layer holds machine-generated LookML: output from the LookML generator, imported block files, or cross-project imports you cannot edit directly.

Put everything regenerable here. When the warehouse schema changes, re-run the generator and replace this layer. Nothing in _base should contain hand-written business logic you would lose on regeneration.

A single monolithic file (_raw.lkml) works well. Leading underscores (_base/, _raw.lkml) sort these files to the top of the IDE file picker, which makes the "do not hand-edit" zone easy to spot.

3. Standard / basic: schema-shaped enrichment

This layer adds declarations that follow directly from the database structure:

  • Primary keys and hidden: yes on surrogate keys
  • Labels and descriptions on raw columns
  • Hiding non-business fields (internal IDs, ETL metadata)
  • Basic explores with many_to_one joins on foreign keys

Keep profit margins, cohort definitions, and domain-specific KPIs out of this layer. If a field requires business judgment rather than schema knowledge, it belongs in a logical layer.

4. Logical layers: business logic by concern

This is where refinements pay off. You can define related logic across multiple views in a single file, grouped by business concept rather than by Looker object.

A profitability layer might add a calculated dimension to orders, define a user_profit derived table, and join that table into the users explore, all in one profit.lkml file. A developer working on margin logic opens one file, not three.

 

Worked example: profit logic across views

This example follows the pattern from the original Looker forum post, updated for a typical ecommerce schema.

layers/profit.lkml

include: "/_standard/orders.lkml"
include: "/_standard/users.lkml"

# Profit logic

view: +orders {
  dimension: profit {
    type: number
    sql: ${price} - ${cost} ;;
    value_format_name: usd
    description: "Line-item profit (price minus cost)."
  }

  measure: total_profit {
    type: sum
    sql: ${profit} ;;
    value_format_name: usd
  }
}

view: user_profit {
  derived_table: {
    explore_source: orders {
      column: user_id { field: orders.user_id }
      column: user_profit { field: orders.total_profit }
    }
  }

  dimension: user_id {
    primary_key: yes
    hidden: yes
  }

  dimension: user_profit {
    hidden: yes
  }
}

explore: +users {
  join: user_profit {
    view_label: "Users"
    sql_on: ${user_profit.user_id} = ${users.id} ;;
    relationship: one_to_one
  }
}

Three objects, one file, one business concern. The base orders and users views in _standard/ stay clean. Regenerating _base/ does not touch any of this.

 

Folder structure and include contract

This is the layout we use on Looker Core delivery engagements:

lookml/
  models/
    ecommerce.model.lkml          # thin: connection + includes only
  _base/
    _raw.lkml                     # regenerable / imported / generated
  _standard/
    orders.lkml                   # PKs, labels, hides, basic FK explores
    users.lkml
  layers/
    profit.lkml                   # business-logic refinements by concern
    cohort.lkml
  explores/
    orders_explore.lkml           # explore-facing refinements / joins (optional)

Include contract, four rules:

  1. Every refinement file must include the files that define the objects it refines. The IDE warns you if you try to refine something that is not in scope. In profit.lkml above, we include the _standard/ versions of orders and users at the top.
  2. The model includes layers in order: _base → _standard → layers/* → explores/*. Later files override earlier ones on conflicting parameters.
  3. Additive parameters accumulate. A refinement that adds a join to an explore does not remove existing joins. A refinement that sets hidden: no on a dimension does override a previous hidden: yes.
  4. Use final: yes when a refinement must not be overridden. Add this flag when you want the IDE to error if a later file tries to change the same object. Useful for locking down imported block customizations.

Table-name overrides belong in the model file (or a dedicated model-level refinement), not scattered across layer files:

view: +orders {
  sql_table_name: analytics.curated.orders ;;
}

Duplicate the model for different environments or datasets by swapping only the model file, not every layer.

 

Operational gotchas

Refinements apply model-wide once included. If a block refinement modifies a shared view (adding period-over-period fields to a date dimension, for example), every explore in that model that uses the view picks up the change. Test at the explore level, not just the view level.

Order matters. Two refinements of the same dimension in one file: the lower one wins. Two layer files in your model: the later include wins. Document your include order in a README or in the model file comments.

Refinements do not replace project governance. Layers help you organize code. They do not replace Git branching, code review, or Looker's content validation. Treat _base/ regeneration as a CI step with a review gate, not a manual click.

Blocks and cross-project imports are where refinements matter most. When you import LookML from a hub project or install a block, you often cannot edit the source files. Refinements let you customize labels, hide fields, and add joins without forking the import.

 

Where this fits in your data platform

If you run a medallion architecture on BigQuery (raw → curated → governed metrics), the layered LookML model mirrors that separation:

Warehouse Layer to LookML Layer Mapping

Warehouse layer LookML layer
Raw / landing tables _base/
Curated tables with typed columns _standard/
Business metrics and KPIs layers/
Domain-specific explores and dashboards explores/ + Looker Core content

Looker Core is the governed semantic layer on top of BigQuery. Keep that layer maintainable and analysts can self-serve without redefining "revenue" in every dashboard. Data Studio handles quick visualization; Looker Core is where reusable business definitions live.

 

Getting started

If your project today is one file per view with business logic mixed into generated code, skip the big-bang refactor. Pick one domain concern (profit, cohorts, inventory) and move it into a layers/ refinement file. Prove the pattern on a low-risk area. Then peel off the next concern.

The syntax has been stable for years. The payoff shows up when your team and your warehouse both grow.

Need help structuring a Looker Core project? Contact Promevo for architecture advisory and delivery services.

 

References

 

data engineering cta

 

Related Articles

From Power BI to Looker: How to Simplify Your Data Stack & Unlock Better Insights

5 min read

From Power BI to Looker: How to Simplify Your Data Stack & Unlock Better Insights

Power BI may be familiar. But it’s not always flexible. You might be hitting limits around cross-cloud reporting, sharing data across teams, or...

Read More
How Gemini in Looker Levels Up Business Intelligence

7 min read

How Gemini in Looker Levels Up Business Intelligence

Business intelligence (BI) continues to evolve as organizations push for faster insights, broader access to data, and smarter decision-making....

Read More
How Promevo Harnesses Looker Internally & For Our Clients

8 min read

How Promevo Harnesses Looker Internally & For Our Clients

Your business data is a gold mine you may not be taking full advantage of. As a business grows in both size and complexity, putting the vast reams...

Read More