Marts
Organised by domain rather than platform. This is the "current state" layer — where SCD2 history collapses to a single row per entity, and where a domain's raw fields turn into named dimensions and facts.
By domain
| Domain | Shape |
|---|---|
web_analytics | GA4-derived facts — traffic, engagement, stock-attributed page views. The largest domain at this layer, mirroring GA4's outsized share of staging. |
crm | Dealer, subscription, lead, and billing dimensions/facts — see CRM domain for the full dim/fact breakdown |
paid_media | Campaign dimension (current-state, from the SCD2 chain) and daily/hourly spend facts |
Two different ideas of what a "dimension" is
paid_media__campaigns_dim and crm__dealer_dim are both called dimensions, but they're built differently, and it's worth knowing which pattern a given mart follows before assuming its shape:
paid_media__campaigns_dimis an SCD2 current-state filter — literallyselect * from {{ ref('paid_media__campaign_scd_int') }} where is_latest = true. The "dimension" here is a snapshot of the SCD2 history table at a point in time, not a separately-modelled entity.paid_media__ad_groups_dimandpaid_media__ads_dimfollow the identical pattern against their own SCD2 intermediates.crm__dealer_dimis a traditional Kimball-style split — the same intermediate model (crm__dealers_int) feeds bothcrm__dealer_dim(descriptive attributes: name, status, region, billing address) andcrm__dealer_fact(measures only: annual revenue, employee count, expected listing). There's no SCD2 involved; this is a straight columnar split by kind.crm__products_dim/crm__products_factandcrm__subscription_dim/crm__subscription_factfollow the same split.
paid_media__campaign_daily_fact adds derived ratios (ctr, avg_cpc, cpa) via a shared safe_divide() macro on top of its intermediate counterpart — the kind of small cross-cutting utility worth knowing exists if you're adding a similar ratio elsewhere.
How data arrives in each mart
Every single mart in this project reads from exactly one intermediate model — there is no mart that joins two intermediates together, and no mart that reads from another mart. Cross-entity joining is deliberately pushed one layer later, into reporting.
Within that constraint, three distinct arrival patterns show up:
- Straight pass-through. The overwhelming majority of marts — most of
crm, most ofweb_analytics, andpaid_media__accounts_dim— are aselect(sometimesselect *) straight off their intermediate, with no transformation at all. - SCD2 current-state filter.
paid_media__campaigns_dim,paid_media__ad_groups_dim, andpaid_media__ads_dimall follow thewhere is_latest = truepattern described above. - Real aggregation. The six
web_analytics__inventorydashboard_cfs_*_factmodels are the exception to "no transformation" — each aggregates the same shared intermediate withSUM(...)and a differentGROUP BY, differentiated only by grain (daily / monthly / by-region / by-dealer-page) and, for the dealer-page variants, an addedWHERE page_path LIKE '%/cars-for-sale/car%'filter.
One pattern that looks like it should exist but doesn't: paid_media__campaign_daily_fact is not a rollup of paid_media__campaign_hourly_fact. Despite both being campaign-grain spend facts, each reads its own separate intermediate model independently — they're parallel siblings, not a daily-from-hourly aggregation chain.
How marts are actually used downstream
Reporting fans out from marts very unevenly — a handful of marts are read by many reporting models across multiple domains, most are read by one or two, and a meaningful number aren't read by any reporting model at all.
Marts several different reporting models depend on:
| Mart | Read by reporting models in |
|---|---|
crm__dealer_dim | inventory and stock |
web_analytics__audience_stock_cfs_daily_fact | inventory and stock |
crm__lead_dim, crm__lead_allocation_dim, crm__lead_allocation_fact | business, crm, inventory, and stock |
web_analytics__audience_country_daily_fact | mostly the business predictor/drilldown family |
web_analytics__channel_daily_fact, channel_session_daily_fact, site_section_daily_fact | mostly the business predictor/drilldown family |
Marts with no reporting-layer consumer found anywhere in the project — these are the layer's most likely direct-BI-query surface, not dead weight, but that can't be confirmed from dbt lineage alone:
crm:crm__dealer_fact,crm__subscription_fact,crm__products_fact,crm__products_dim,crm__billing_cycle_dimpaid_media:paid_media__accounts_dim,paid_media__ad_groups_dim,paid_media__ads_dim,paid_media__account_daily_fact,paid_media__campaign_hourly_factweb_analytics:web_analytics__audience_country_hourly_fact,web_analytics__audience_section_channel_daily_fact
A few reporting models skip the mart layer entirely, reading the raw intermediate instead of the mart built on top of it:
business__daily_decay_rptandbusiness__detailed_drilldown_hourly_rptboth readweb_analytics__audience_country_hourly_intdirectly — bypassingweb_analytics__audience_country_hourly_fact, which is exactly the orphaned hourly mart listed above.stock__market_overview_monthly_rptreadsweb_analytics__inventorydashboard_cfs_daily_intdirectly, bypassing its own mart.
(inventory__inventory_master_sheet_rpt also bypasses this project's marts chain, but by reading STOCK_FACT_MART from a different project entirely — see Reporting layer.)