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 inlayers/ - 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.
Model File
Orchestration only. Connection, includes, no business logic.
Base / Raw
Machine-generated, safe to regenerate. Never hand-edit.
Standard / Basic
Schema-shaped enrichment: PKs, labels, hides, basic joins.
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: yeson surrogate keys - Labels and descriptions on raw columns
- Hiding non-business fields (internal IDs, ETL metadata)
- Basic explores with
many_to_onejoins 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:
- 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.lkmlabove, we include the_standard/versions ofordersandusersat the top. - The model includes layers in order:
_base → _standard → layers/* → explores/*. Later files override earlier ones on conflicting parameters. - Additive parameters accumulate. A refinement that adds a join to an explore does not remove existing joins. A refinement that sets
hidden: noon a dimension does override a previoushidden: yes. - Use
final: yeswhen 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
- Fabio (Looker), "Organizing your LookML into layers with our new refinement syntax", Google Developer forums, April 2020
- LookML refinements, Google Cloud documentation
- Reusing code with extends, Google Cloud documentation
