← All Articles
Semantic DataXpress  ·  Data Modeling

Your Warehouse Is Already Modeled. Stop Re-Modeling It.

The hidden tax of modern semantic layers, and what it looks like to skip the bill.

Narendra Devarasetty  ·  2026
8 min read

Every modern semantic layer arrives with the same promise: one place for metric definitions, governed analytics, self-serve for the business. Six months in, the same thing happens at every company: the data team's biggest job is no longer the warehouse. It is the second model they built on top of it.

The difference, in one line
In this article
  1. The hidden tax of modern semantic layers
  2. The schema they expect vs the warehouse you actually have
  3. The architectural divide: author-time vs runtime
  4. What the tax actually costs you
  5. A different bet: meet the warehouse where it is
  6. The problems most layers punt on
  7. The same hard case, two ways
  8. What this looks like in practice
  9. What you stop doing

The hidden tax of modern semantic layers

Pick any data team that has rolled out a semantic layer in the last five years. Ask them how much of their roadmap, in any given quarter, is spent on the layer itself. The answer is almost always most of it. Building views. Building the semantic models the tool needs. Building the marts those tools needed. Reconciling drift between the warehouse schema and whatever idiom the tool insists on.

The promise was governed self-serve. The reality is a parallel modeling project: a team that used to maintain warehouse transformation models now also maintains a second set of semantic-layer definitions, plus the persistent derived tables and aggregate tables those tools demanded to perform acceptably. The semantic layer was supposed to compress the work. It often doubles it.

This is not a critique of any particular tool. The pattern is uniform across them, because they all share an architectural assumption: that you will re-describe your warehouse in the tool's idiom. The shape of your warehouse, the patterns your data engineers chose for good reasons, becomes friction the layer must work around.

The tax is not the YAML

If the cost was just writing config, every team would happily pay it. The tax is the second-order work: the flat marts you build because the tool can't handle a fan-out, the derived tables you maintain because performance dropped, the duplicated metric logic when one team needs a slightly different lens, the on-call rotation for a system whose cache has its own opinions about correctness.

· · ·

The schema they expect vs the warehouse you actually have

There is a reason every semantic-layer tutorial uses the same example. One central fact table, surrounded by dimension tables. Clean many-to-one joins. One grain per fact. One row per entity in each dim. The textbook star schema. Every product screenshot, every getting-started page, every example project: they all look like this.

Real warehouses do not look like this.

The textbook vs the warehouse
STAR SCHEMA What the tools were designed against FACT DIM DIM DIM DIM One fact. One grain. Clean many-to-one joins. YOUR WAREHOUSE What you actually have Transaction Fact Event Stream Journal Fact Line-item Fact Accumulating Aggregate Regular Dim SCD Type 2 Snapshot Dim Periodic Snap Bridge (M:N) State Trans. Different grains. Different temporality. Different join semantics. All in the same warehouse.

A serious warehouse contains a mix of physical patterns, each chosen for a reason. Some dimensions are current-state lookups; others are historically versioned with effective dates; others are daily snapshots. Some facts are clean transactions; others are append-only journals with signed amounts; others are accumulating snapshots that update in place. Each pattern was selected because it is the right shape for the data it holds, and each pattern carries different rules about how it can be queried safely.

Dimensions are not all the same kind of thing.

Dimension flavor
What it captures
What semantic layers assume
01
Regular (Type 1)
Current state. One row per entity. Overwritten on change.
The default. Works fine.
02
SCD Type 2
Full history. Multiple rows per entity with effective_from / effective_to.
Flattened upstream into "as-of" marts. The temporality is lost.
03
Periodic snapshot dim
State captured daily/weekly. Composite grain (date × entity).
Reshaped into a current-state lookup, or double-counted.
04
Bridge
Many-to-many resolution between two entities.
Structural fan-out: usually denormalized away.

Facts are not all the same kind of thing either.

Fact flavor
What it captures
What semantic layers assume
01
Transaction fact
One row per business event. Single grain. Additive measures.
The default. Works fine.
02
Line-item fact
Sub-grain of a transaction. One row per line, not per order.
Joined onto the header into a wide mart to escape fan-out.
03
Event / activity stream
High-volume sub-second events. Semi-additive.
Pre-aggregated upstream into a daily summary.
04
Journal / ledger
Append-only with signed amounts and CASE-based metrics.
Pre-aggregated. CASE semantics flatten poorly.
05
Accumulating snapshot
One row per entity, milestone columns, updated as state advances.
Treated as a transaction fact. Milestone semantics lost.
06
Periodic snapshot fact
Fact-level state captured daily/weekly. Composite grain.
Treated as a transaction fact. Double-counted across snapshots.
07
Aggregate / rollup
Pre-rolled, often with row-exclusion restrictions.
Routed into without checking eligibility: silent wrong numbers.
08
State transition fact
Status-from / status-to events.
Treated as a generic event stream; transitions lose meaning.

When the tool assumes star and the warehouse is a mix, something has to give. The thing that gives is always the warehouse. The SCD2 dim gets flattened into an "as-of" mart. The journal gets pre-aggregated into a daily summary. The line-item fact gets joined onto the order header into a wide denormalized table. The snapshot dim gets reshaped into a current-state lookup. Each reshape is a small, irreversible loss of fidelity, paid for by the data engineering team in marts they did not want to build, refresh schedules they did not want to maintain, and a warehouse that quietly stops looking like itself.

"Other tools assume a star. Real warehouses are a mix. The mismatch is paid for by the warehouse."

DataXpress does something different. It lets each table be what it is. The model declares the type of each table (current-state dim, SCD Type 2 dim, transaction fact, journal, snapshot, accumulating, bridge) and the compiler queries each one correctly. No "as-of" mart for the SCD2. No daily summary for the journal. No wide denormalization for the line-items. The warehouse stays a mix because it is a mix.

· · ·

The architectural divide: author-time vs runtime

Strip away the YAML and the marketing language, and every semantic layer answers one question: where does the burden of correctness live? The answer is the architectural divide. It is also the cleanest way to see what makes DataXpress different.

Concern
Most semantic layers
DataXpress
01
Where correctness is authored
At author-time. The modeler writes joins, aggregate-awareness rules, derived tables, and metric variants ahead of every question users might ask.
At runtime. The generator inspects the question, classifies the joins, and rewrites the SQL safely on the fly.
02
Fan-out joins
Author's responsibility. Build a flat mart, write a symmetric-aggregate workaround, or restrict the explore.
Generator's responsibility. Detected via BFS over active joins; the offending fact is rewritten as an inline pre-agg subquery.
03
SCD2 temporal joins
Usually flattened upstream into an "as-of" mart. The semantic layer sees a current-state dim.
First-class. Relationship declares type: scd; generator emits the temporal predicate against the fact date.
04
Multi-hop dimensions
Author declares the path explicitly per explore, or denormalizes the dim onto the fact.
BFS over the relationship graph resolves the path. No path declaration required.
05
Allocation across grains
Not a concept. The user gets either a flattened mart or a wrong number.
First-class. Metrics declare proportional_to or equal_split; generator emits an allocator subquery.
06
Unanswerable combinations
Often produce an inflated number with no warning.
Structured refusal. FanoutInflationError with the metric, dimensions, and offending relationship; the agent re-plans.
07
Performance strategy
Pre-aggregations / derived tables / aggregate tables that the modeler must define, schedule, and reconcile.
Aggregate-source routing is opt-in. You add aggregates when the data team chooses to, not because the tool needs them to be fast.
08
Drift between schema and model
Manual reconciliation; CI checks layered on top.
Physical layer validated against the live database at load. Mismatches are caught before any user query runs.

The pattern is uniform across the rows. Other semantic layers push correctness onto the modeler at author-time. The modeler has to anticipate every fan-out, every grain mismatch, every temporal join, every multi-hop, and bake the answer into the model before users start asking. DataXpress moves the burden into the generator. The model declares what's there; the generator decides how to query it safely. This is the difference, and every other difference in this article descends from it.

Said another way

Other tools give you a vocabulary for describing a warehouse you have to reshape to fit them. DataXpress gives you a compiler that reads the warehouse you already have and generates correct SQL against it.

If your warehouse already uses Kimball patterns (SCD2, periodic snapshots, accumulating snapshots, journals, bridges) the compiler model is the one that costs you nothing extra to adopt.

· · ·

What the tax actually costs you

The visible cost of a semantic layer is the model files. Look harder, and the real cost is everywhere else.

Two sources of truth. Your warehouse has a schema. Your semantic layer has a description of it. The two drift the moment a column is renamed or a table is rebuilt. Every team I've watched roll out one of these tools eventually adopts a CI check that compares the two, an explicit acknowledgement that the layer can lie about what the warehouse contains.

A new role. "Analytics engineer" was supposed to bridge data engineering and analysis. In practice, the role has expanded to include the semantic layer itself: someone has to know the modeling idiom, the derived-table lifecycle, the cache refresh schedule. This is real, valuable work. It is also work that did not exist before the layer was introduced.

Flat marts as a workaround. The most expensive cost is the one nobody attributes to the semantic layer directly. When the tool's join model can't safely handle a fan-out (an order fact joined to its line items, a customer joined to a SCD2 address history) the path of least resistance is to flatten the data into a one-row-per-grain mart and point the semantic layer at that. Your warehouse stops being shaped for the business and starts being shaped for the tool. Every flattening is a small, irreversible loss of fidelity.

"The warehouse ends up shaped for the tool, not the business. Every flattening is a small, irreversible loss of fidelity."

Pre-aggregation strategies you maintain instead of write. Most layers ship some form of aggregate awareness, pre-aggregation, or caching. Each one solves the same problem: the tool's straightforward path is too slow on real data, so you maintain a parallel pipeline of materializations to make it fast. These materializations have their own freshness windows, their own correctness rules, and their own ways of silently returning stale numbers.

The patterns your warehouse already uses don't translate. Slowly changing dimensions, periodic snapshots, accumulating snapshots, bridge tables, journal-style ledgers: these are not exotic. They are the fourteen patterns Ralph Kimball described in 1996, and they are how serious warehouses are built. Most semantic layers handle two or three of them well. The others get flattened, denormalized, or worked around.

· · ·

A different bet: meet the warehouse where it is

DataXpress starts from a different premise. Your data engineers already modeled the warehouse. They chose a transaction fact for orders, a periodic snapshot for inventory, an SCD Type 2 for the customer address history, a journal for payments, a bridge for promotions. Each of those choices carries information (about grain, about temporality, about cardinality) that the warehouse already encodes.

So we don't ask you to write a second model. We ask you to write a thin description of the model that already exists.

Two postures toward the warehouse
MOST SEMANTIC LAYERS Semantic Layer Idiom Views / Models / Metrics Flat Marts & Rollups Reshape to fit the tool Your Warehouse Reshaped & flattened Three layers. Two sources of truth. Constant drift. DATAXPRESS Thin Context Layer Describes what's already there Your Warehouse SCD2, snapshots, journals, bridges, accumulating facts, unchanged Two layers. One source of truth. The warehouse stays itself.

The thin context layer does three things, and only three things. It declares what tables exist and what their columns mean. It declares the relationships between them, including cardinality, join keys, and time-versioning rules. It declares the metrics, formulas, and dimensions analysts care about. Nothing in this layer reshapes the warehouse. Nothing in this layer pre-aggregates. Nothing in this layer asks the data engineering team to change how they model.

Everything else (fan-out detection, allocation, multi-hop dimension joins, SCD2 temporal resolution, aggregate routing) happens in the query generator. Not as a configuration burden you carry. As capability that ships with the tool.

That is the whole bet. The dozen-plus Kimball patterns a production warehouse actually contains (SCD2, periodic and accumulating snapshots, journals, bridges, state-transition facts) are absorbed by the generator, not pushed back to you as marts to maintain. A semantic layer's real value is not how elegant its config looks; it is how many of these patterns it handles without asking you to reshape the data.

· · ·

The problems most layers punt on

Patterns are the easy half. The hard half is what happens when they interact: when the user asks a question that crosses grains, traverses multiple hops, or implicates a slowly-changing dimension. Here is what DataXpress does at the generator level so you don't have to flatten the warehouse to escape it.

Fan-out joins, rewritten as subqueries

The classic silent failure: an order header has total_amount, the line items have quantity. A user asks for "total revenue and total units." A naive join inflates revenue by the line-item count, the classic fan-out. The hard part is getting it right for non-additive measures too (AVG, COUNT DISTINCT, CASE-based metrics), not just SUM.

DataXpress detects fan-out edges via a BFS over the active joins, then rewrites the offending fact as an inline subquery: (SELECT join_key, SUM(measure) FROM fact WHERE … GROUP BY join_key). Filter pushdown is automatic. Multiple fan-outs in the same query are pre-aggregated independently. AVG decomposes into SUM/COUNT before the rewrite so the semantics survive.

Allocation when the question crosses grains

Sometimes the user genuinely wants a header measure broken down by a many-side dimension: revenue by promotion, where promotions live in a bridge table. There is no safe join. There is only an allocation policy.

Metrics declare allocation: { proportional_to: line_item_fact.amount } or allocation: { equal_split: { many_table: order_promotion_bridge } }. The generator emits an allocator subquery joining the header to the weight grain. The agent layer surfaces the allocation rule to the planner so it knows when a request is safe to attempt.

Multi-hop dimension joins

"Revenue by product category" reaches three tables: order header, line items, products. Most semantic layers force you to either declare the path explicitly or denormalize the dim onto the fact. DataXpress runs a BFS over the relationship graph in find_join_path() and resolves the path itself. The dim's dimJoinHops is surfaced to the planner so it understands when a request requires traversal.

SCD2 temporal resolution

A customer address history has multiple rows per customer with effective_from / effective_to. Joining "as of now" is wrong: the join must resolve to the row that was active when the order was placed. The relationship declares type: scd with the date columns, and the generator emits the temporal predicate against the fact's transaction date. No materialized "address as-of" mart. No flat denormalization.

Chasm trap detection and structured refusal

Some combinations have no safe rewrite: two facts on opposite sides of a shared dimension, no allocation declared, no aggregate available. The generator detects these via primary-side reachability BFS and raises a structured FanoutInflationError with the metric, dimensions, and offending relationship. The agent catches this and re-plans with the alternatives surfaced in the error. The user never sees a silently inflated number.

· · ·

The same hard case, two ways

The cleanest way to see the difference is to take one question that has no answer in flat-mart land and watch what each approach requires you to do. The question: "Total revenue and total units, by product category, last quarter." Revenue lives on the order header. Units live on the line items. Product category is a multi-hop dim through the line item.

The typical workaround: what you buildorders_line_items_mart.sql
-- The two facts can't be crossed safely, so you
-- pre-aggregate the order header to its own grain
-- and materialize a wide mart the tool can read.
SELECT
  l.order_id,
  l.product_id,
  l.quantity,
  o.total_amount / o.line_count AS revenue_per_line  -- approximation
FROM orders o
JOIN order_lines l USING (order_id);

-- and now you own it: a refresh schedule, a freshness
-- window, and a place where stale numbers can live.
-- Often one such mart per analytical question.
DataXpress: what you writeom_order.yaml
# No mart. No pre-aggregation to maintain.
# Two metrics declared on their native fact, with grain.
metrics:
  - name: total_revenue
    formula: "SUM(om_order_fact.total_amount)"
  - name: total_units
    formula: "SUM(om_order_line_item_fact.quantity)"

dimensions:
  - name: product_category
    column: om_product_dim.category
    # multi-hop dim resolved by BFS, no path written

# That's it. The generator handles fan-out detection,
# subquery pre-aggregation, and the multi-hop join
# at runtime. No reshaping of the warehouse.

The first is the familiar shape: you, the modeler, anticipate the fan-out and write a workaround. A derived table, a pre-aggregation, a wide mart. It becomes a maintenance object: a refresh schedule, a freshness window, a place where stale data can live. And it shapes the warehouse a little further toward the tool.

The second is shorter not because we left things out, but because the burden of correctness is in the generator, not in the modeler's head. You declare two metrics on their native facts. The generator does the rest at the moment a user asks the question.

"Other tools ask you to anticipate every hard query at author-time. DataXpress asks the warehouse to stay itself, and answers hard queries at runtime."

· · ·

What this looks like in practice

The most concrete way to see the difference is a single query. The user asks for two metrics from two different facts in the same perspective: total revenue from the order header, total units from the line items.

A user asks

"Show me total revenue and total units, by product category, for last quarter."

In a typical semantic layer, this is the moment a flat mart gets built. The warehouse already has the data, but the tool can't safely cross from the order header to the line items to the product dimension without inflating revenue. So someone, somewhere, materializes orders_with_line_items_with_product, a denormalized table whose only job is to make the BI tool happy. The warehouse drifts a little further from how the data engineering team actually models orders.

DataXpress generates this directly against the warehouse:

Generated SQL: subquery pre-aggregation, no mart
SELECT
  product.category                                AS product_category,
  SUM(order_preagg.total_amount)             AS total_revenue,
  SUM(line_item.quantity)                    AS total_units
FROM om_order_line_item_fact line_item
JOIN om_product_dim product
  ON line_item.product_id = product.product_id
JOIN (
  -- pre-aggregated to the order grain so the join
  -- to line_item does not inflate total_amount
  SELECT order_id, SUM(total_amount) AS total_amount
  FROM om_order_fact
  WHERE order_date BETWEEN '2026-01-01' AND '2026-03-31'
  GROUP BY order_id
) order_preagg
  ON line_item.order_id = order_preagg.order_id
WHERE line_item.order_date BETWEEN '2026-01-01' AND '2026-03-31'
GROUP BY product.category
ORDER BY total_revenue DESC;

Three things to notice. The order header is pre-aggregated to its own grain inside an inline subquery, so the join to the line items doesn't inflate revenue. The line-item filter is pushed into the subquery so the pre-aggregation is correct under the user's date range. No CTEs, no window-function tricks: just a subquery any SQL reviewer can trace back to the model. And critically: the warehouse did not change. om_order_fact stayed a transaction fact. om_order_line_item_fact stayed a line-item fact. om_product_dim stayed a current-state dim. No mart was built.

· · ·

What you stop doing

The clearest way to see what a tool gives you is to look at what it lets you stop doing. Here is what falls off your roadmap.

Most of these are not items on a Jira backlog. They are the background hum of a data team's quarter: the work that gets done because the tool needs it, the work that everyone has come to assume is just part of running a semantic layer. Removing them changes what the team gets to spend time on.

· · ·

Vocabulary vs compiler

The bet behind DataXpress is structural, not stylistic. We are not arguing that other semantic layers are badly designed. We are arguing that they all answer one architectural question the same way (push correctness onto the modeler at author-time), and that the cost of that answer is paid in places that don't show up in the marketing material: in the marts, the derived tables, the refresh schedules, the new role on the team, the warehouse that quietly reshapes itself to fit the tool.

DataXpress answers the question the other way. The model declares what's there. The generator decides how to query it safely. Fan-out, allocation, multi-hop, SCD2, chasm-trap: all moved out of the modeling layer and into the compiler, where one good implementation serves every question users will ever ask. You stop authoring workarounds for queries you haven't seen yet.

"Other semantic layers are vocabularies. DataXpress is a compiler. The warehouse you have is the model. We just compile against it."

Your data engineers already chose a transaction fact for orders. They already chose an SCD Type 2 for the customer address history, a journal for payments, a bridge for promotions, a periodic snapshot for inventory. Each of those choices was made for good reasons, in service of the business. A semantic layer that asks you to flatten those choices, or to redescribe them in a different vocabulary, is a layer that has misunderstood where the modeling already lives.

The warehouse you have is the model. We just compile against it.

Read: Why your semantic layer matters more than your AI agent →
Read: The full technical deep-dive →