In practice you build these layers with dbt. You write SELECT statements, dbt turns them into tables or views, and ref() points one model at another. From those references dbt works out the build order.
A marketing mart is usually pre-aggregated. Not every event, but one row per day per channel. That buys you two things: the dashboard loads fast, and you scan megabytes instead of gigabytes in BigQuery, where you pay per byte scanned.
-- models/marts/marketing/mart_channel_performance.sql
{{ config(materialized='table', partition_by={'field': 'date_day', 'data_type': 'date'}) }}
with sessions as (
select date_day, channel, count(distinct session_id) as sessions
from {{ ref('int_ga4_sessions') }}
group by 1, 2
),
orders as (
select date_day, channel, count(*) as orders, sum(revenue_ex_vat) as revenue
from {{ ref('int_orders_attributed') }}
where is_test_order = false
group by 1, 2
),
spend as (
select date_day, channel, sum(cost) as cost
from {{ ref('stg_ads__daily_cost') }}
group by 1, 2
)
select
sessions.date_day,
sessions.channel,
sessions.sessions,
coalesce(orders.orders, 0) as orders,
coalesce(orders.revenue, 0) as revenue,
coalesce(spend.cost, 0) as cost
from sessions
left join orders using (date_day, channel)
left join spend using (date_day, channel)
Notice what is already decided here: test orders are out, revenue excludes VAT, and channel attribution happens in the model below. Whoever opens this dashboard cannot accidentally make those choices differently.