dbt-ga4: the community standard for modelling the GA4 BigQuery export
A dbt package that turns the raw GA4 BigQuery export into report-ready models for sessions, users, ecommerce and attribution.
The Measurement Room
A BigQuery ecommerce funnel across view_item, add_to_cart, begin_checkout and purchase, counting distinct users per step and the step-to-step drop, using conditional aggregation instead of fragile joins.
The GA4 funnel exploration is fine until you want to change one thing about it, at which point you are fighting the interface. In BigQuery the whole funnel is one query, and once it is yours you can move the steps, change the window, or split it by anything the export carries. This is the four-step ecommerce funnel: view_item, add_to_cart, begin_checkout, purchase, counted as distinct users at each step with the drop-off between them.
The approach is conditional aggregation, not joins. Chaining four self-joins on user_pseudo_id is slow and it multiplies rows in ways that quietly inflate counts; counting distinct users per event in a single pass is faster, simpler, and far harder to get subtly wrong. It is read-only.
analytics_XXXXX, where XXXXX is your GA4 property ID; daily tables are events_YYYYMMDD, queried with the events_* wildcard.your_project.analytics_XXXXX with your own project and dataset._TABLE_SUFFIX date filter controls how many daily tables BigQuery scans, and therefore the cost. Keep the window to the period you actually want to report.One scan, four counts. Each step's MAX(IF(...)) flags the users who fired that event and the SUM counts them; the rates divide each step by the one before, and by the top of the funnel:
-- Ecommerce funnel: distinct users per step and step-to-step rates.
-- Replace your_project.analytics_XXXXX with your project and dataset.
-- Conditional aggregation over one pass, no self-joins.
WITH user_steps AS (
SELECT
user_pseudo_id,
MAX(IF(event_name = 'view_item', 1, 0)) AS did_view_item,
MAX(IF(event_name = 'add_to_cart', 1, 0)) AS did_add_to_cart,
MAX(IF(event_name = 'begin_checkout', 1, 0)) AS did_begin_checkout,
MAX(IF(event_name = 'purchase', 1, 0)) AS did_purchase
FROM `your_project.analytics_XXXXX.events_*`
-- _TABLE_SUFFIX controls how many daily tables are scanned, and the cost.
WHERE _TABLE_SUFFIX BETWEEN '20260601' AND '20260607'
AND event_name IN
('view_item', 'add_to_cart', 'begin_checkout', 'purchase')
GROUP BY user_pseudo_id
),
steps AS (
SELECT
SUM(did_view_item) AS view_item_users,
SUM(did_add_to_cart) AS add_to_cart_users,
SUM(did_begin_checkout) AS begin_checkout_users,
SUM(did_purchase) AS purchase_users
FROM user_steps
)
SELECT step, users, rate_from_previous, rate_from_top FROM (
SELECT 1 AS ord, 'view_item' AS step, view_item_users AS users,
1.0 AS rate_from_previous, 1.0 AS rate_from_top
FROM steps
UNION ALL
SELECT 2, 'add_to_cart', add_to_cart_users,
SAFE_DIVIDE(add_to_cart_users, view_item_users),
SAFE_DIVIDE(add_to_cart_users, view_item_users)
FROM steps
UNION ALL
SELECT 3, 'begin_checkout', begin_checkout_users,
SAFE_DIVIDE(begin_checkout_users, add_to_cart_users),
SAFE_DIVIDE(begin_checkout_users, view_item_users)
FROM steps
UNION ALL
SELECT 4, 'purchase', purchase_users,
SAFE_DIVIDE(purchase_users, begin_checkout_users),
SAFE_DIVIDE(purchase_users, view_item_users)
FROM steps
)
ORDER BY ord;
your_project.analytics_XXXXX with your project and dataset.IN (...) list and the four MAX(IF(...)) lines._TABLE_SUFFIX dates, check the estimated scan size, then run.rate_from_previous is the step-to-step conversion, rate_from_top is cumulative from view_item.rate_from_previous is where the funnel leaks; a big drop from add_to_cart to begin_checkout points at the cart, a drop from begin_checkout to purchase at the checkout itself. rate_from_top is the honest end-to-end figure you can quote.
Some limitations to keep in mind, because this counts unique users, not ordered journeys:
user_pseudo_id is a device, not a person, so cross-device and cookie-cleared journeys fragment, and consent-denied traffic may be modelled or absent. The rates are directionally reliable and useful for spotting the leak; treat them as that, not as an exact count of shoppers.More Like This
A dbt package that turns the raw GA4 BigQuery export into report-ready models for sessions, users, ecommerce and attribution.
Three foundation queries for the GA4 BigQuery export: the event_params unnesting pattern, daily users and sessions, and top pages by view, all with a date filter that controls scan cost.
Session-scoped source, medium and campaign reporting from the GA4 BigQuery export, aggregating sessions and conversions per channel, with an honest note on why it will not tie out to the GA4 UI.