Skip to main content
This guide walks you through building a thin dimensional spine in Omni — a modeling pattern that lets you analyze many independent event tables together by time and shared entity attributes without incurring fanout or poor performance. While the examples below use mobile gaming events (logins, sessions, ads, purchases), this pattern applies to any domain with multiple event streams sharing common dimensions, such as SaaS product events, marketplace activity, or IoT telemetry.

When to use this pattern

Use this pattern when:
  • You have many event tables sharing common dimensions like user_id.
  • Analysts want a single Topic for analysis across multiple event streams.
  • Queries typically touch only a subset of those event streams.
  • Performance and predictable SQL generation are priorities.

How to model thin spine tables

A common approach to multi-fact analysis is to model a massive join of calendar x users x all event tables. This quickly leads to fanout, slow queries, and unpredictable SQL. Instead, you can model a thin dimensional spine — a table where each row represents the existence of activity, not the details of the activity itself. Instead of this:
Avoid this pattern, which joins everything together
You model this:
Use this pattern, which creates a thin spine of activity
Which you could use to create a spine table like dim__user_event_spine_thin. This table contains one row per (event_date_day, user_id, event_type) combination that actually occurred:
Using Snowflake? Cluster your spine by (event_date_day, event_type) to improve query performance.

Modeling a secondary date spine

In addition to the thin spine, you should maintain a date-only spine table (dim__date_spine) for calendar descriptors. This gives you a clean place for all calendar fields without bloating the thin spine. To build it:
  • Fields - Include attributes like is_weekday, is_us_federal_holiday, or fiscal calendar fields.
  • Join logic - Join to the topic on:

Storage considerations

While the spine is logically thin, it can still become large in absolute storage terms. Keep these factors in mind:
  • Row counts - Each (user, time grain, event_type) combination produces a row
  • Granularity - Hourly (or finer) spines significantly multiply row counts
  • Pruning - Pruning helps query performance, but not storage footprint
To keep storage manageable:
  • Default to daily granularity unless finer-grain analysis is truly required
  • Use separate spines for different grains (e.g., a daily user spine and an hourly session spine)
  • Be deliberate about which event types you include in the spine

Spine materialization benefits

You may wonder whether it’s worth materializing the spine given its potential size. While the spine can grow large, the cost is a known and bounded trade-off:

Build-time cost

The spine intentionally shifts cost from query-time to build-time

Reduce effective scan size

Pruning makes the effective scan much smaller than the full table size

Improve predictability

CTE-only patterns can complicate cohort analysis and reusability

Semantic stability

The spine creates a stable semantic surface for downstream analysis

Build and model the spine with dbt and Omni

In this section, we’ll walk you through how to build and materialize the thin spine table using dbt and then model it in Omni. In this guide, we’re using the following event tables:
  • Engagement - event__login, event__session_start
  • Monetization and ads - event__iap_purchase, event__ad_impression
  • Shared fields - All event tables share user_id and event_ts (timestamp)

Requirements

To follow along, you’ll need:
  • Familiarity with dbt
  • Familiarity with modeling in Omni
1

Build a generate_event_spine_from_models dbt macro

First, you’ll build a generate_event_spine_from_models macro in dbt. This pattern uses the macro to derive event_type from model names and emit a UNION ALL spine query.The macro expects:
  • Event models are named like event__login, event__iap_purchase, etc.
  • event_type is the suffix after event__.
  • Each event model includes event_ts and user_id columns, or you override these via arguments
generate_event_spine_from_models macro
2

Build an incremental dim__user_event_spine_thin dbt model

The next step is to build a dim__user_event_spine_thin model in dbt.The spine model is incremental and supports two modes:
  1. Append-only - Only inserts new spine rows beyond what already exists
  2. Merge with a 7-day lookback window - Recomputes the most recent 7 days and merges into the target
dim__user_event_spine_thin model
3

Build the topic in Omni

Once the spine is built, configure your Omni topic to use it as the base view. This topic uses the secondary date spine for calendar descriptors and enforces a date filter for safety.
user_activity.topic file
4

Curate the topic fields

To make sure users filter consistently on the date spine, hide timestamps and non-common dimensions within the event views:
Snippet of user_activity.topic
5

Define relationships

The core philosophy of this pattern is to keep the thin spine minimal — it contains only join keys, event_type, and dates. All other metadata is joined in from external views.There are two key architectural decisions to understand:
  • Prevent unnecessary symmetric aggregation - Event relationships are explicitly set to one_to_one even though the actual cardinality is one_to_many. This prevents Omni from unnecessarily employing symmetric aggregation.
  • Minimal base - Many teams hide most or all dimensions on the thin spine so users primarily interact with date fields from the date spine, attributes from attr_ tables, and measures from event_ views.
Every additional event table follows this same structure. Repeat this pattern when you add new events.
Omni Relationships file
6

Build an example analysis

With the spine materialized and the Omni model built, you can try out what you’ve created by building an analysis. In this example, you’ll build a cross-event analysis that looks at Week x Country with multi-event measures.In an Omni workbook, create a query with the following configuration:
  • Dimensions - dim__date_spine.date_day (grouped by week) and attr__users_profile.country
  • Measures - event__login.login_count, event__session_start.session_count, event__ad_impression.ad_impression_count, and event__iap_purchase.iap_revenue
  • Derived metric - avg_logins_per_user, calculated by dividing login count by spine-level distinct users
7

Optional: Add new event streams

This pattern is designed to make adding new event types mechanical and low-risk. To add a new event stream:
  1. dbt - Add the event model name to event_models in the spine SQL
  2. Omni topic - In the Omni topic, add event__new_event: {} to the joins section
  3. Pruning - Add a case when {{ event__new_event.in_query }} then 'new_event' end line to always_where_sql.
  4. Relationships - In the Omni model’s relationships file, add a conditional one_to_one relationship join matching on event_type, user_id, and date grain

Best practices for modeling spines in Omni

Now that you’ve walked through modeling spines in Omni, we have a few tips to reduce surprises in aggregation and filtering. Keep the following in mind when modeling in Omni.

General tips

  • Hide non-common dimensions - Expose only curated measures and optionally timestamps for drilling
  • Prefer curated metrics - Instead of raw fields like purchase_amount, expose measures like iap_revenue or payer_count
  • Keep cohort-defining fields in attribute space - Put cohort keys in attr__ tables so they behave consistently across event streams

Apply topic best practices

As your model grows, topics can become difficult to use if the field list gets too large or too heterogeneous. Keep these guidelines in mind:
  • Focus over mega-topics - Prefer multiple focused topics (e.g., engagement, monetization) over a single mega-topic
  • User-facing product - Treat topics as user-facing products with coherent scopes
  • Spine reusability - The thin spine is infrastructure — you can reuse it across multiple topics
For more topic design guidance, refer to the Topic design best practices guide.

Use filtered measures for event-specific breakdowns

Instead of exposing event-specific dimensions directly, create curated filtered measures. For example, if event__login has a login_method dimension, you can create specific measures for counts of Google or iCloud logins:
event__login.view (illustrative)

Build a cross-event analysis

With this pattern in place, you can build cross-event analyses like a weekly report by country. You would use:
  • Dimensions - dim__date_spine.date_day (grouped by week) and attr__users_profile.country
  • Measures - event__login.login_count, event__session_start.session_count, event__ad_impression.ad_impression_count, and event__iap_purchase.iap_revenue
  • Derived metrics - For example, avg_logins_per_user calculated by dividing login count by spine-level distinct users

Next steps

Now that you understand thin dimensional spines and how to build your own, you can use them to build different types of analysis. For example, start with the Build cohort analysis with thin spine guide.