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...
6 min read
Shane Selterre
|
Published: August 6, 2026
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
+ prefix to modify a view or explore without editing the base file_base/; business logic lives in layers/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.
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.
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 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.
Orchestration only. Connection, includes, no business logic.
Machine-generated, safe to regenerate. Never hand-edit.
Schema-shaped enrichment: PKs, labels, hides, basic joins.
Business logic grouped by concern, not by Looker object.
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"
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.
This layer adds declarations that follow directly from the database structure:
hidden: yes on surrogate keysmany_to_one joins on foreign keysKeep 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.
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.
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.
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:
profit.lkml above, we include the _standard/ versions of orders and users at the top._base → _standard → layers/* → explores/*. Later files override earlier ones on conflicting parameters.hidden: no on a dimension does override a previous hidden: yes.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.
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.
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.
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.
Meet the Author
Shane Selterre is a Senior Solutions Architect at Promevo, where he helps enterprise teams design and deliver governed analytics using Looker Core and BigQuery. Bringing an extensive background in data infrastructure from past roles at CSX Technology, TD Finance, Bytecode IO, and Nerdery, you can find Shane presenting at live events like Promevo’s Atlanta Masterclass series. Outside of architecting Google Cloud solutions, he co-hosts a podcast covering retro video games.
5 min read
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...
7 min read
Business intelligence (BI) continues to evolve as organizations push for faster insights, broader access to data, and smarter decision-making....
8 min read
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...