Comparison with Other Semantic Layers¶
How OrionBelt Semantic Layer (OBSL) stacks up against the leading semantic layer / metrics tools. These pages are honest, two-sided comparisons including gap analyses in both directions — useful when evaluating which tool fits your stack.
At a glance¶
| OBSL | dbt SL | Malloy | LookML | Cube | AtScale | |
|---|---|---|---|---|---|---|
| License | Source-available (BSL) | Definitions OSS; runtime in dbt Cloud | Open source | Proprietary | Apache 2.0 (core) + Cube Cloud | Proprietary; free Developer Community Edition for non-prod |
| Self-hostable | ✅ | Definitions yes, runtime no | ✅ | ❌ | ✅ | ✅ (licensed) |
| Standalone (no transformation tool dep.) | ✅ | ❌ requires dbt | ✅ | ✅ | ✅ | ✅ |
| Format | YAML (OBML) |
YAML on dbt models | DSL (.malloy) |
DSL (.lkml) |
YAML / JS + Twig | Visual designer |
| Query interface | OrionBelt Semantic QL (OBSQL) + Arrow Flight SQL + PostgreSQL wire + REST + DB-API | GraphQL/JDBC (Cloud) | Malloy language | Looker UI / API | Cube SQL API (Postgres-wire) + REST + GraphQL | MDX + DAX + JDBC/ODBC + REST |
First-class cumulative metric (running / rolling / grain-to-date, partitionBy) |
✅ | ✅ (no partitionBy) |
Per-query | Partial | Partial (rolling_window) |
Via MDX |
| First-class period-over-period metric | ✅ | Via offset_window |
Per-query | Via table calc | Query-time time_shift |
Via MDX |
| First-class window metric (rank / lag / lead / ntile / first_value / last_value) | ✅ declarative | ❌ | Per-query calculations | Table calcs (UI-side) | Via type: number + raw SQL |
Via MDX calculated members |
Statistical / regression aggregates as first-class measure types (stddev, variance, corr, covar_*, regr_*) |
✅ 9 declarative aggregations (arity-validated, per-dialect gated) | Basic stddev only as first-class |
Basic stddev only as first-class |
Via type: number + raw SQL |
Via type: number + raw SQL |
Via MDX calculated members |
String / list aggregation (listagg) |
✅ first-class listagg (per-dialect STRING_AGG/GROUP_CONCAT/LISTAGG/arrayStringConcat; DISTINCT + WITHIN GROUP ordering) |
❌ no string-agg measure type | ✅ string_agg helper |
✅ list measure type |
✅ generic string type wraps aggregate SQL |
❌ semi-additive only |
| Conversion / funnel metrics | ❌ | ✅ | Patterns | Patterns | Patterns | Patterns |
| Symmetric aggregates | ❌ (uses CFL) | ❌ | ✅ | ✅ | ✅ | ✅ (OLAP) |
Hierarchical subtotals (WITH ROLLUP / WITH CUBE) |
✅ first-class in Semantic QL (trailing modifier + GROUPING() flag columns; dialect-portable across all 8 drivers) |
❌ presentation concern | ❌ | UI-only checkbox; no LookML construct | "Rollup" means pre-aggregation tables, not the SQL operator | Via MDX/DAX |
| Natural SQL query surface | ✅ Semantic QL (OBSQL) with explicit MEASURE() marker + aggregate-wrap matching — over Arrow Flight SQL AND PostgreSQL wire (v2.5.0+) |
❌ | n/a (Malloy DSL) | n/a (LookML DSL) | ✅ Cube SQL API (Postgres-wire) | n/a (MDX/DAX) |
| Read-only governance — no raw-SQL or write-op escape hatch | ✅ closed by design: raw SQL → RAW_SQL_REJECTED, DDL/DML → WRITE_OPERATION_REJECTED, catalog probes answered from the model |
dbt SL is read-only by API shape; no raw-SQL surface | Malloy emits its own queries | Looker SQL Runner allows raw SQL | Cube SQL API rejects writes; raw SELECTs flow through | AtScale routes through MDX/DAX |
| Multi-rooted modeling (peer-rooted facts) | ✅ independent dataObjects |
✅ independent semantic_models |
❌ single-rooted source |
❌ single-rooted explore (joined facts only) |
✅ independent cubes | ✅ multiple facts in one Cube via conformed dims |
| Multi-fact query plan | UNION ALL legs (CFL) |
FULL OUTER JOIN on shared entities |
n/a | JOIN inside explore (symmetric agg) | Per-fact subqueries FULL JOINed on shared dimensions (multi-fact views, Tesseract, v1.7+) |
JOIN with OLAP aggregation |
| Multi-path joins (between same pair) | ✅ per-query selection via pathName + usePathNames |
❌ no path-name primitive | ❌ aliased sources (model-time) | from aliasing (model-time) |
Dijkstra + member-type priority heuristic; pin via views |
Role-playing dimensions (model-time) |
| Composability discovery — given a partial query, which artefacts can still be added | ✅ ACR (composables endpoint) — bidirectional, member-level, fanout-aware and CFL-aware; governed REST surface over a multi-fact model + guided UI highlighting |
Partial — programmatic but metric-anchored & asymmetric: dimensions-for-metrics (mf list dimensions --metrics, GraphQL dimensionsPaginated(metrics:), JDBC metrics_for_dimensions) |
✅ headless query-builder ASTQuery.getInputSchema() ("fields that can be added") — editor-oriented, single-rooted source scope |
❌ only static per-field attributes via lookml_model_explore; picker not selection-aware |
❌ /meta is a static schema dump; incompatible members surface only as a query-time error |
❌ implicit in cube relationships / conformed dimensions; no composability API |
| Nested / hierarchical results | ❌ | ❌ | ✅ (nest) |
❌ | ❌ | ❌ |
| OLAP hierarchies (multi-level, parent-child) | ❌ | ❌ | ❌ | Partial | Partial — level-based hierarchies + view folders; no parent-child / ragged |
✅ first-class |
| Custom granularities / fiscal calendars | ❌ fixed TimeGrain set |
✅ custom_granularities on a time spine |
❌ | Via dimension_group + derived tables |
✅ custom granularities + calendar cubes (4-5-4, fiscal) | ✅ first-class time dimensions |
| MDX / Excel pivot tables | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ unique |
| RDF/SPARQL graph view | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| MCP server | ✅ | ✅ (dbt-mcp) | ✅ (Publisher) | ✅ managed Looker MCP server + MCP Toolbox (2026) | ✅ | ✅ |
| Interactive playground / UI | ✅ Gradio (incl. RDF ontology graph) | dbt Cloud Studio (paid) | VS Code + Publisher | ✅ Looker IDE | Cube Playground / Studio | ✅ Design Center |
| Notebook authoring (VS Code / Colab) | ✅ quickstart.ipynb runs natively in VS Code or Colab |
Via dbt-cli in any notebook | Notebook tutorials | ❌ | ❌ | ❌ |
| Built-in BI dashboards | ❌ | ❌ | VS Code | ✅ Looker | ❌ | Via MDX in Tableau/Excel |
| Pre-aggregations / materialization | ❌ | Via dbt models | ❌ | ✅ (PDTs) | ✅ flagship | ✅ autonomous |
| Result cache | ✅ file cache based on freshness inheritance (TTL from dataObject.refresh; ETL heartbeat invalidation) |
dbt Cloud query cache | ❌ | Looker query cache (persist_for) + aggregate awareness |
Tiered (in-memory + optional Redis + Cube Store) with refresh-key TTL | Built-in cache + autonomous aggregates |
| Row-level security in model | ❌ | Via dbt | ❌ | ✅ | ✅ (query_rewrite) |
✅ enterprise |
| Multi-tenancy primitives | Sessions only | Cloud-managed | ❌ | ❌ | ✅ first-class | ✅ enterprise |
| OSI interoperability | ✅ converter | ❌ | ❌ | ❌ | ❌ | ✅ founding contributor |
Detailed comparisons¶
- vs. dbt Semantic Layer (MetricFlow) — coupled to dbt projects, served via dbt Cloud
- vs. Malloy — a query language with semantic modeling, plus the Publisher REST/MCP server
- vs. LookML / Looker — the proprietary modeling language behind Google Cloud Looker
- vs. Cube — the OSS production semantic layer with pre-aggregations and multi-API parity (Postgres wire, REST, GraphQL)
- vs. AtScale — the enterprise universal semantic layer with native MDX/DAX for Excel and Power BI live connections
Topology: a recurring theme¶
Most semantic layers assume a single-rooted, tree-shaped model (one fact at the center, dimensions fanning out). OBSL is built on a directed join graph (DAG) that supports:
- Star and snowflake schemas (the common cases)
- Multi-rooted models — query across multiple unrelated facts in a single semantic surface, resolved via the CFL (Composite Fact Layer) planner that emits
UNION ALLlegs - Multi-path joins — multiple valid join paths between the same pair of objects, named via
pathNameand selected per query viausePathNames - Cycle detection — explicit, not silent
This matters when your warehouse doesn't fit a clean star: you need ship-address vs. billing-address joins to the same dimension, or a single API surface that exposes revenue and support tickets together. See the Compilation Pipeline guide for how this flows through the planner.
How each tool actually handles multi-fact queries¶
Two separate questions matter here, and they cut differently across tools:
- Modeling-time: does the language let you declare independent peer entities (multiple facts) with their own joins to shared dimensions, or are joins scoped to a single base context (one explore, one source) so peer-rooted topology can't be expressed at all?
- Query-time: when a query asks for measures from multiple facts, is the SQL plan a single
JOINgraph (with fanout risk, mitigated by symmetric aggregates) orUNION ALLlegs?
A constraint that's universal: every tool here requires you to declare entities (tables/cubes/sources/dataObjects) first — you can't ad-hoc add a join to an undefined table. The differences are about how those declared entities can be wired together.
| Tool | Peer-rooted modeling | Multi-fact query plan |
|---|---|---|
| OBSL | ✅ dataObjects are independent peers, each with its own joins |
UNION ALL legs (CFL) with NULL-padding for missing measures |
| dbt SL (MetricFlow) | ✅ semantic_models are independent peers; joins via shared entities |
FULL OUTER JOIN on the shared entity (per dbt's join-logic.md) |
| Malloy | ❌ every source is single-rooted; join_one / join_many always presume one root |
n/a (separate queries) |
| LookML | ❌ joins live inside one explore (one base view + joined views) |
JOIN inside the explore (symmetric agg) |
| Looker (runtime) | n/a | merged_results merges two queries' results in the API layer |
| Cube | ✅ cubes are independent peers with joins blocks; planner traverses the cube graph (Dijkstra) — shortest path is not always the semantically correct one |
Multi-fact views (Tesseract, GA + default in Core v1.7): one aggregating subquery per fact, FULL JOINed on the shared dimensions. Single-fact queries still resolve to one JOIN path with symmetric aggregates |
| AtScale | ✅ multiple fact datasets in one Cube connected by conformed dimensions; virtual cubes compose Cubes | JOIN with OLAP-style aggregation |
So peer-rooted modeling isn't unique to OBSL — Cube, dbt MetricFlow, and AtScale all support it. Single-rooted-only sits with Malloy and LookML, by language design.
The multi-fact query plans split three ways, and the correctness story is non-trivial for two of them:
-
UNION ALL(OBSL) — one leg per fact; each legSELECT … FROM fact_n JOIN dims … GROUP BY …. The two facts are never in the sameFROMclause, so the chasm trap (cross-product when both facts have multiple rows per shared dim key) cannot occur by construction. No symmetric-aggregate accounting required. The cost is two SQL passes, one per leg. -
Pre-aggregated subqueries joined on shared keys (dbt MetricFlow, Cube) — each fact is aggregated on its own first, then the results are joined on the conformed columns. Correctness depends on that pre-aggregation: if
saleshas 3 rows for user_1 andreturnshas 2, a rawFROM sales FULL OUTER JOIN returns ON user_idproduces 6 rows and inflated sums. Both engines aggregate each side before the join, so the join is over one row per key per side.- dbt MetricFlow joins peer fact tables
FULL OUTER JOINon the shared entity (per itsjoin-logic.md). - Cube does the same in multi-fact views (Tesseract, GA and default-on in Core v1.7): one independent subquery per fact joining only the tables that fact needs,
FULL JOINed on all common dimension columns,NULLwhere a fact has no matching row. It requires each fact cube to declare a direct join to every shared dimension at a root-level join path, and the join-key granularity to match theGROUP BYgranularity.
- dbt MetricFlow joins peer fact tables
-
Single JOIN path with symmetric aggregates (LookML, AtScale, and Cube within one fact) — planner picks one path through the join graph (explore-base-rooted in LookML; OLAP aggregation in AtScale; Dijkstra in Cube) and wraps each measure's aggregation in symmetric-aggregate SQL (DISTINCT-aware SUMs, COUNT(DISTINCT …) over surrogate keys, etc.) to undo the fanout the JOIN introduces. Works for many common shapes — but it's a correction during aggregation rather than a structural fix, and edge cases (multiple unique-distinct columns, nested aggregations, mixed COUNT/SUM) can still produce surprising numbers. Cube's own docs recommend
viewsfor "join predictability and stability" — i.e., pin the path at model-design time rather than trusting the planner to pick correctly.
OBSL's UNION ALL plan trades extra SQL passes for the absence of an entire class of fanout problem. Cube's and MetricFlow's pre-aggregate-then-join plan reaches the same safety through a different shape, at the cost (in Cube's case) of a stricter modeling requirement on how facts reach conformed dimensions.
For multi-path joins between the same pair of entities (e.g., Order.ship_address_id vs Order.bill_address_id both pointing to Address):
- OBSL: declare both with
secondary: true+pathName; pick at query time viausePathNames. Per-query selection — unique here. - Cube: graph allows multiple paths; planner resolves with Dijkstra + a member-type priority heuristic (measures → dimensions → segments → time dimensions); pin via a
viewfor predictability. Graph-aware but not per-query selectable — and the choice of shortest path is a graph-theoretic answer to what is fundamentally a semantic question. When a query asks for "orders by address," shortest-path picks one ofship_address_id/bill_address_idbased on edge weights, not on the consumer's intent. The two paths may yield genuinely different correct answers; Dijkstra picks the one the heuristic prefers. Views can pin the choice, but only at model-design time, not per query. - AtScale: role-playing dimensions — same dim aliased into multiple roles (
OrderDate,ShipDate). Model-time aliasing. - LookML:
fromaliasing — same view declared twice in an explore under different aliases. Model-time. - Malloy: aliased sources (
source: ship_addr is address). Model-time. - dbt MetricFlow: no first-class path-name primitive.
What's structurally distinct about OBSL after the corrections:
UNION ALLplan avoids the chasm trap by construction — the two facts never share aFROMclause, so cross-product fanout is impossible. LookML's and AtScale's single-JOIN-path approach has to correct for fanout with symmetric aggregates; CFL avoids needing a correction at all. (MetricFlow's and Cube's pre-aggregated-subquery joins are also structurally safe here — this point separates OBSL from the symmetric-aggregate tools, not from those two.)- Per-query path selection for multi-path joins — Cube has graph-aware multi-path resolution (heuristic) and can pin a path in a view, but OBSL is the only one where the consumer of the query picks the path explicitly. Shortest-path is a graph-theoretic answer to a semantic question.
- No direct-join requirement between facts and conformed dimensions — CFL anchors each leg on a common root found by graph search, so a fact can reach a shared dimension through intermediate hops. Cube's multi-fact views require every fact cube to join each shared dimension directly, at a root-level join path.
- Static fanout detection as an explicit error class (
compiler/fanout.pyraisesFanoutError) — wrong-direction many-to-one joins fail at compile time rather than producing silently-wrong row counts. Where structure alone cannot decide — a measure sourced from the one side of a join, whose rows the join replicates — the grain-dedup pass aggregates it over rows deduplicated on that object's key rather than over the flattened join. That rewrite now also composes with derived metrics (components split across dedup CTEs and rebuilt in the outer projection), withHAVING(predicates on a deduplicated measure move to the outerWHERE), and withtotal/ cumulative / window metrics; what it cannot express is refused with a named reason rather than silently inflated.
AtScale's conformed-dimensions-within-a-Cube + virtual cubes, and Cube's multi-fact views since Core v1.7, are the closest peers in modeling capability. Nothing in this comparison set matches OBSL's combination of UNION-ALL multi-fact plan + per-query path selection.
Where OBSL fits best¶
- Embedded analytics in a SaaS product where consumers (apps, agents, BI tools) need a stable JSON Query API and you don't want to ship a DSL interpreter.
- Multi-tenant semantic models with TTL-scoped sessions.
- LLM/agent integration via MCP — a clean, schema-driven query surface beats teaching the agent a new language.
- Modern cloud warehouses including ClickHouse, Databricks, Dremio, and DuckDB.
- Open-source / self-hostable / air-gapped deployments.
Where another tool may be a better fit¶
- dbt SL if you've standardized on dbt and want metrics tightly coupled to your transformation pipeline, with dbt Cloud governance.
- Malloy for analyst-driven exploration and BI authoring, especially if you need hierarchical (
nest) result shapes. - Looker if you're buying an end-to-end BI platform with dashboards, alerts, RLS, and PDTs — and the per-user licensing fits your org.
- Cube if you need pre-aggregations for sub-second analytics on large datasets, first-class multi-tenancy/RLS, or a curated view layer with hierarchies, folders, and fiscal calendars — and you're willing to operate (or pay for Cube Cloud to operate) the heavier runtime. (PostgreSQL-wire BI connectivity is no longer a Cube exclusive — OBSL also speaks it as of v2.5.0. Multi-fact querying is no longer an OBSL exclusive either — Cube Core v1.7 ships multi-fact views.)
- AtScale if your business users live in Excel pivot tables and need native MDX, or you need DAX for Power BI live connections — no other tool in this comparison set speaks those protocols.
These tools are not mutually exclusive — it's plausible to ship a BI platform (Looker / AtScale) for the human audience and OBSL alongside it for the embedded / API / agent audience.
About OSI¶
Several comparisons reference OSI (Open Semantic Interchange) — an open standard for portable semantic models, founded to let metric and dimension definitions move between BI tools, semantic layers, and data platforms without rewriting. See open-semantic-interchange.org for the specification.
OBSL ships bidirectional converters (POST /v1/convert/osi-to-obml, POST /v1/convert/obml-to-osi) and Import/Export buttons in the Gradio playground. AtScale is a founding contributor to the OSI initiative; the other tools in this comparison do not currently support OSI directly. See the OSI Interoperability guide for usage details.