OBSL vs dbt Semantic Layer (MetricFlow)¶
A feature comparison between OrionBelt Semantic Layer (OBSL) and the dbt Semantic Layer (powered by MetricFlow). Captured 2026-05-01.
TL;DR¶
- dbt SL wins on: conversion metrics, the
metric_timevirtual dimension, ecosystem maturity, and governance/lineage tied to the dbt transformation pipeline. - OBSL wins on: being a self-hostable, warehouse-agnostic, transformation-tool-agnostic engine; richer modeling topologies (multi-rooted DAG, named alternative join paths) where dbt assumes single-rooted star/snowflake; explicit multi-fact CFL planning; an RDF/SPARQL view of the model; a clean REST surface; and a more ergonomic period-over-period metric type.
- Different niches: dbt SL is "metrics on top of your dbt project, served by dbt Cloud." OBSL is "drop-in semantic compiler you can embed anywhere, modeling-tool independent."
1. Modeling philosophy¶
| Aspect | OBSL (OBML) | dbt Semantic Layer |
|---|---|---|
| Source of truth | Standalone YAML (OBML) — independent of any transformation tool |
YAML coupled to dbt models; semantic models reference dbt ref()s |
| Top-level objects | dataObjects, dimensions, measures, metrics, filters |
semantic_models (entities, measures, dimensions) + metrics |
| Object scoping | Each DataObject has its own columns:; dimensions/measures/metrics live at model scope and reference {[DataObject].[Column]} |
Dimensions/measures/entities are scoped inside each semantic_model; metrics reference measures |
| Identity for joins | Explicit joins between data objects with columnsFrom/columnsTo, joinType, secondary, pathName |
Implicit: entities of type primary/foreign/unique/natural; MetricFlow auto-resolves joins by matching entity names |
| Deployment | Self-hosted FastAPI service, MCP server, Gradio UI; OSS | Definitions in dbt Core OSS; query API gated behind dbt Cloud |
2. Metric types¶
OBSL MetricType enum (src/orionbelt/models/semantic.py):
| OBSL | dbt SL | Notes |
|---|---|---|
Measure (sum/avg/count/min/max, total: bool for grand totals) |
simple metric over a measure |
Both first-class |
Metric type: derived with {[Measure A]}/{[Measure B]} expression |
ratio, derived |
Both first-class |
Metric type: cumulative (running total, rolling window, grain-to-date) |
cumulative (running, period-to-date, rolling) |
Both first-class — see §3 |
Metric type: period_over_period with 4 comparison modes |
Approximated via offset_window and metric_time |
OBSL has a dedicated metric type — see §4 |
| — | conversion |
Gap in OBSL |
OBSL coverage: ~95% of dbt's metric expressivity, missing only conversion.
3. Cumulative metrics (parity)¶
Implementation: src/orionbelt/compiler/cumulative_wrap.py (~230 lines), pipeline phase placed after PoP wrap so it operates on already-compared data when needed.
Three patterns supported, all dbt-equivalent:
metrics:
# 1. Running total (unbounded cumulative sum)
- name: revenue_running_total
type: cumulative
measure: revenue
timeDimension: order_date
cumulativeType: sum
# 2. Rolling window (e.g. last 7 days)
- name: revenue_7d_avg
type: cumulative
measure: revenue
timeDimension: order_date
cumulativeType: avg
cumulativeWindow: 7
# 3. Grain-to-date (e.g. month-to-date, resets each month)
- name: revenue_mtd
type: cumulative
measure: revenue
timeDimension: order_date
cumulativeType: sum
grainToDate: month
Window functions used:
| Pattern | SQL produced |
|---|---|
| Running total | SUM(x) OVER (ORDER BY time) |
| Rolling window | SUM(x) OVER (ORDER BY time ROWS BETWEEN N-1 PRECEDING AND CURRENT ROW) |
| Grain-to-date | SUM(x) OVER (PARTITION BY DATE_TRUNC(grain, time) ORDER BY time) |
Aggregations: sum, avg, min, max, count (CumulativeAggType).
Grains for grain-to-date: year, quarter, month, week (GrainToDate).
4. Period-over-Period (OBSL advantage)¶
Implementation: src/orionbelt/compiler/pop_wrap.py (~510 lines).
OBSL exposes PoP as a first-class metric type with a comparison-mode enum, where dbt requires composing offset_window + metric_time + a derived metric.
metrics:
- name: revenue_yoy
type: period_over_period
measure: revenue
periodOverPeriod:
grain: month # bucket grain
offset: -1 # compare vs. previous period
offsetGrain: year # one year earlier
comparison: percentChange # ratio | difference | previousValue | percentChange
Comparison modes (PeriodOverPeriodComparison):
ratio— current / priordifference— current − priorpreviousValue— just the prior period's valuepercentChange— (current − prior) / prior
Internally builds a synthetic date range and joins current vs. comparison period.
vs. dbt: dbt typically requires:
1. Define a simple metric.
2. Reference it in a derived metric using metric_time and offset_window parameters.
3. Compose ratio/percent-change manually in expr.
OBSL's single-declaration approach is more ergonomic for the common case.
5. Multi-fact / fanout¶
| OBSL | dbt SL | |
|---|---|---|
| Fanout detection | Explicit compiler/fanout.py raises FanoutError |
Avoided implicitly via entity types |
| Multi-fact strategy | Dedicated CFL (Composite Fact Layer) planner — compiler/cfl.py — emits UNION ALL legs with per-leg common-root resolution via JoinGraph.find_common_root() |
MetricFlow join planner traverses entity graph; strategy is internal/opaque |
| User control | Explicit star-vs-CFL switch surfaces in compilation | Not exposed |
| Snowflake optimization | Uses UNION ALL BY NAME |
n/a |
OBSL's CFL is more transparent and inspectable; dbt's approach is more declarative but harder to reason about for complex join graphs.
6. Data modeling topology (a major differentiator)¶
dbt SL is fundamentally single-rooted, tree-shaped in practice: each query resolves a base measure to a primary entity and traverses outward via matching entity names. Multi-fact queries work, but the topology is implicit and ambiguous multi-paths are a smell rather than a feature.
OBSL is built on a directed join graph (DAG) with explicit support for richer topologies:
| Topology | Star (single fact + dims) | Snowflake (chained dims) | Multi-rooted (multiple facts) | Multi-path (alt. joins between same pair) | Cycles |
|---|---|---|---|---|---|
| OBSL | ✅ | ✅ | ✅ via CFL UNION ALL legs with per-leg common root |
✅ first-class via secondary: true + pathName + per-query usePathNames |
Detected and rejected |
| dbt SL | ✅ | ✅ | Partial — works if entities line up, but no explicit multi-fact planner | Workaround: define alternate entities and pick by relationship | Implicit |
Why this matters: Real-world warehouses are messy. You routinely need a customer→order→order_item path and a customer→returns path queryable in one model, or to choose between "ship_address_id" and "billing_address_id" joins to the same address dimension on a per-query basis. dbt expects you to flatten these into well-shaped entities upstream; OBSL lets you model them as-is and resolve at query time.
7. Joins¶
| OBSL | dbt SL | |
|---|---|---|
| Definition | Directed join declarations with columnsFrom/columnsTo, joinType, secondary, pathName |
Inferred by matching entity names across semantic models |
| Multiple paths between same objects | First-class via secondary: true + named pathName, selected per-query via usePathNames: [{source, target, pathName}] |
Express via additional entities — no path naming primitive |
| Cycle / multi-path validation | Built into resolver; pathName required for secondary |
n/a (graph traversal handles) |
8. Dialects / execution¶
| OBSL | dbt SL | |
|---|---|---|
| Dialect coverage | 8: BigQuery, ClickHouse, Databricks, Dremio, DuckDB, MySQL, Postgres, Snowflake | All warehouses dbt supports (broader) |
| SQL generation | Full custom AST → SQL with per-dialect codegen (dialect/*.py) |
MetricFlow → SQL |
| Execution surface | Self-hosted, runs anywhere | Query API (JDBC/GraphQL/Python SDK/MCP) dbt Cloud only |
| dbt Core query API | n/a | Definitions only — no built-in runtime serving |
9. APIs / interfaces¶
| OBSL | dbt SL | |
|---|---|---|
| REST API | Yes — full session lifecycle, validate/compile/execute, ER diagram, find, lineage explain, OSI conversion |
No (REST not offered) |
| Arrow Flight SQL | Yes — gRPC server on port 8815; BI tools (DBeaver, Tableau JDBC, Power BI ODBC) connect natively | No |
| DB-API 2.0 drivers | Yes — 8 drivers (ob-bigquery, ob-snowflake, ob-postgres, ob-mysql, ob-duckdb, ob-clickhouse, ob-databricks, ob-dremio) |
No |
| GraphQL | No | Yes (dbt Cloud) |
| JDBC | Via Arrow Flight SQL JDBC driver | Yes (dbt Cloud) |
| MCP | Yes (in-tree thin client + standalone repo orionbelt-semantic-layer-mcp) |
Yes (dbt-mcp) |
| Python SDK | Via FastAPI client | Yes |
| UI / Playground | Yes — interactive Gradio playground: SQL Compiler, Query Results table, auto-generated Mermaid ER diagrams, interactive RDF/OBSL ontology graph (vis-network), OSI import/export, settings panel | dbt Cloud Studio (paid) |
| RDF graph + SPARQL | Yes (/graph, /sparql) |
No |
| Format conversion | OSI ↔ OBML round-trip (/convert/*) |
n/a |
10. Other distinctives¶
| Feature | OBSL | dbt SL |
|---|---|---|
| Sessions / multi-tenant runtime | TTL, max-age, rate limits, 410/429 | Cloud-managed |
| Caching | None built-in | dbt Cloud query cache |
| Versioned governance, lineage to upstream models | No (model is standalone) | Strong — inherits dbt's lineage, tests, docs, exposures |
| Filter ergonomics | MeasureFilter, FilterContext, GrainOverride, query-level where/having |
Per-metric filter:, metric_time |
| Vendor-agnostic | Yes — pure OSS | Practical lock-in: production query APIs require dbt Cloud |
11. Gap analysis (OBSL → dbt parity)¶
To match dbt SL feature-for-feature, OBSL would need:
conversionmetric type — funnel-style metric: count of base events that converted to a target event within a window.metric_timevirtual dimension — a unified time axis across heterogeneous fact tables, abstracting per-table date columns. (Partially achievable today via dimensions on each data object, but not as a single canonical handle.)- (Nice to have) Caching layer — query result cache keyed on (model version, query, filters).
- (Nice to have) GraphQL or JDBC surface — for BI tool integration parity.
Conversely, dbt SL would need to add to match OBSL's strengths:
- Self-hostable query runtime (currently dbt Cloud only).
- RDF/SPARQL graph view of the model.
- Named secondary join paths for non-trivial multi-path scenarios.
- Explicit fanout detection / CFL planner exposure.
References¶
- OBSL
MetricTypeenum:src/orionbelt/models/semantic.py - OBSL cumulative wrap:
src/orionbelt/compiler/cumulative_wrap.py - OBSL PoP wrap:
src/orionbelt/compiler/pop_wrap.py - OBSL CFL planner:
src/orionbelt/compiler/cfl.py - OBSL docs: Model Format, Period-over-Period Metrics, Compilation Pipeline
- dbt SL: https://docs.getdbt.com/docs/build/about-metricflow
- dbt SL GraphQL API: https://docs.getdbt.com/docs/dbt-cloud-apis/sl-graphql