← Back to all posts GA4 to BigQuery customer intelligence pipeline showing data flow and analytics marts

GA4 + BigQuery: Customer-Intelligence Pipeline Patterns

Jun 2026 · Data Engineering

The Google Analytics 4 to BigQuery export is the single most useful — and most underused — asset in a modern analytics stack. The reporting interface inside GA4 is built for aggregate dashboards: sampled, cardinality-limited, and locked to a fixed set of dimensions. The BigQuery export is the opposite: raw, unsampled, event-level data you own outright and can join to anything. That difference is exactly what customer intelligence needs.

This is a practitioner guide to the patterns I use when building customer-intelligence pipelines on GA4 + BigQuery for enterprise clients. It covers the export schema essentials that trip people up, the staging models that turn raw events into something queryable, and the analytical marts — sessionization, identity stitching, RFM, lifetime value, and cohorts — that the whole pipeline exists to produce. No client data appears here; the patterns are general and the SQL is illustrative.

Quick answer: Turn on the native GA4 BigQuery export, then build three layers on top of the raw events_* tables: a staging layer that flattens nested event parameters and rebuilds sessions, an identity layer that stitches user_pseudo_id to a stable user_id, and a marts layer that produces per-customer RFM, LTV, and cohort tables. Schedule it with dbt or scheduled queries and you have customer intelligence the GA4 UI cannot give you.

Why GA4 + BigQuery for Customer Intelligence

GA4 was rebuilt around an event model — every interaction is an event with parameters, rather than the session-and-hit model of Universal Analytics. That event model is awkward in the reporting UI but ideal in a warehouse. Three things make the BigQuery export the right foundation:

GA4 Events BigQuery Export raw · unsampled Customer Intelligence CRM · orders · product
GA4 exports every event to BigQuery unsampled; joining that export to CRM, order, and product tables is what turns raw events into customer intelligence.

The export itself is free for standard properties, including both the daily batch table and the streaming intraday table. You pay only normal BigQuery storage and query costs once the data lands — and partition pruning keeps those costs low if you model carefully.

The Pipeline in Three Layers

The architecture that holds up over time is a layered one. Each layer has a single job and feeds the next. Resist the urge to write one giant query against the raw export — it becomes unmaintainable the first time the schema surprises you.

1. Raw / source

The untouched GA4 export: events_YYYYMMDD daily tables plus events_intraday_*. Never edited. This is your replayable source of truth.

2. Staging

Flatten event_params and user_properties, cast timestamps, rebuild sessions. One clean, typed row-per-event model the rest of the pipeline can trust.

3. Identity

Resolve devices to people. Map every user_pseudo_id to a stable customer key, stitching in user_id wherever a login event provides one.

4. Marts

The analytical outputs: sessions, funnels, RFM segments, lifetime value, and cohort retention — keyed on the resolved customer, ready for BI and activation.

GA4 Export Schema: The Parts That Matter

The export lands in a dataset named analytics_<property_id>, with one partitioned table per day (events_YYYYMMDD) and a streaming table (events_intraday_YYYYMMDD) when streaming export is enabled. Each row is a single event. The fields that drive every downstream model:

Field Type What it is
event_name STRING The event: page_view, session_start, purchase, and your custom events.
event_timestamp INT64 Event time in microseconds since the Unix epoch. Divide by 1e6 before casting to a timestamp.
event_params RECORD, REPEATED Key/value array. The value sits in one of string_value, int_value, float_value, double_value. Holds ga_session_id, page_location, and more.
user_pseudo_id STRING Device/client identifier from the GA client_id cookie. Always present; resets on cookie clear or device switch.
user_id STRING The stable identifier you set on login. Present only when your site/app assigns it. The anchor for identity stitching.
user_properties RECORD, REPEATED Sticky user-scoped attributes (plan tier, lifecycle stage). Same nested key/value shape as event_params.
items RECORD, REPEATED Ecommerce line items on purchase/cart events: item id, name, price, quantity. The source of monetary value.
traffic_source / collected_traffic_source RECORD Acquisition attribution: source, medium, campaign. First-touch vs. event-level depending on the field.

The two facts that cause the most confusion: timestamps are in microseconds, and there is no session table. GA4 does not store sessions as rows — you reconstruct them from events. Both are handled in the staging layer below.

event_params repeated record string_value int_value float_value double_value
event_params is a repeated record — you UNNEST it and read the one typed value field (string, int, float, or double) that matches the parameter key.

Staging: Flatten Events and Rebuild Sessions

Because event_params is a repeated record, you pull a single parameter by unnesting it and selecting the typed value. This subquery pattern is the workhorse of the entire pipeline:

SELECT
  event_date,
  TIMESTAMP_MICROS(event_timestamp)            AS event_ts,
  event_name,
  user_pseudo_id,
  user_id,
  -- pull the session id out of the nested params
  (SELECT value.int_value FROM UNNEST(event_params)
     WHERE key = 'ga_session_id')              AS ga_session_id,
  (SELECT value.string_value FROM UNNEST(event_params)
     WHERE key = 'page_location')              AS page_location,
  -- a globally-unique session key: device + session number
  CONCAT(
    user_pseudo_id, '.',
    CAST((SELECT value.int_value FROM UNNEST(event_params)
            WHERE key = 'ga_session_id') AS STRING)
  )                                            AS session_key
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260101' AND '20260131'

Two disciplines make this layer durable:

Always filter on _TABLE_SUFFIX (the date sharded into the table name) so BigQuery prunes to only the days you need. Querying events_* without a suffix filter scans your entire history and is the number-one source of surprise bills.

The session keyuser_pseudo_id concatenated with ga_session_id — is what GA4 itself uses internally to count sessions, and it is globally unique. Group by it to rebuild sessions: first and last event timestamp give session start/end and duration, the count of page_view events gives depth, and the presence of a purchase event flags a converting session. That single sessions model feeds funnels, engagement metrics, and attribution downstream.

page_view page_view purchase session_key = pseudo_id + session_id Session start · end · depth · converted
GA4 has no session table — group events sharing a device and ga_session_id into a session_key, then derive start, end, depth, and whether the session converted.

Identity Resolution: Devices to People

This is the layer that separates real customer intelligence from page-view reporting. user_pseudo_id answers "which browser?"; customer intelligence needs "which person?" The mapping is rarely one-to-one — a single customer shows up as many pseudo-ids across devices and cookie resets, and a shared device can carry several.

The stitching pattern: for every user_pseudo_id that ever co-occurs with a known user_id (because the user logged in during at least one event), propagate that user_id to all of that device's events. Build a crosswalk table:

-- one row per device, resolved to a stable customer key where known
SELECT
  user_pseudo_id,
  -- the most recent non-null user_id seen on this device
  ARRAY_AGG(user_id IGNORE NULLS ORDER BY event_timestamp DESC LIMIT 1)[SAFE_OFFSET(0)]
    AS resolved_user_id
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260101' AND '20260131'
GROUP BY user_pseudo_id

Downstream, the customer key is COALESCE(resolved_user_id, user_pseudo_id) — use the real person when known, fall back to the device when not. Every per-customer metric keys on that. The honest caveat worth stating in any deliverable: stitching is a heuristic. Anonymous-before-login behavior only attaches to a person retroactively, and cross-device coverage is only as good as your login rate. Name those limits rather than implying the resolution is perfect.

pseudo_id: dev-123 no login seen pseudo_id: dev-456 user_id: alan42 (login) crosswalk: propagate known user_id to every pseudo_id customer_key = COALESCE(user_id, pseudo_id)
A login event on one device is enough to resolve every pseudo_id that device has ever used to a single, stable customer_key.

The Marts: What Customer Intelligence Actually Produces

With clean sessions and resolved customers, the analytical marts are straightforward to express. These are the four that earn their keep on almost every engagement.

RFM Segments

Start here

Recency, Frequency, Monetary per customer, scored into quintiles. Turns raw purchase events into named segments: champions, loyal, at-risk, lapsed. The fastest path from data to action.

Lifetime Value

Revenue lens

Summed purchase value per resolved customer, optionally projected forward. Anchors acquisition spend and retention priorities to real revenue, not proxies.

Cohort Retention

Trend lens

Group customers by first-seen week or month, then track return/purchase rate over subsequent periods. Shows whether product and lifecycle changes actually move retention.

Funnel / Conversion

Behavior lens

Ordered event sequences (view → add-to-cart → checkout → purchase) measured per session and per customer. Pinpoints exactly where intent leaks out.

RFM is the highest-leverage starting point because it is cheap to compute and immediately actionable. From the resolved purchase events:

WITH purchases AS (
  SELECT
    COALESCE(resolved_user_id, user_pseudo_id)   AS customer_key,
    TIMESTAMP_MICROS(event_timestamp)            AS purchase_ts,
    (SELECT value.double_value FROM UNNEST(event_params)
       WHERE key = 'value')                      AS purchase_value
  FROM staged_events
  WHERE event_name = 'purchase'
)
SELECT
  customer_key,
  DATE_DIFF(CURRENT_DATE(), DATE(MAX(purchase_ts)), DAY)  AS recency_days,
  COUNT(*)                                                AS frequency,
  SUM(purchase_value)                                     AS monetary
FROM purchases
GROUP BY customer_key

Wrap each of recency, frequency, and monetary in NTILE(5) OVER (...) to score quintiles, then map the score triples to segment names. That single table drives lifecycle email, paid-audience exclusions, and win-back campaigns — the activation side of customer intelligence.

purchase events RFM quintile scores champions · at-risk · lapsed lifecycle email paid-audience exclusion win-back campaign
RFM segments aren't the end product — they're a routing table into lifecycle email, paid-audience exclusions, and win-back campaigns.

Scheduling and Orchestration

The export refreshes daily (intraday if streaming is on). The pipeline should refresh on the same cadence. Two common approaches:

Approach Best for Trade-off
dbt Teams wanting version-controlled models, tests, lineage, and documentation More setup; needs a runner (dbt Cloud, Cloud Run, or scheduled CI)
BigQuery scheduled queries Small pipelines, fast start, no extra tooling No tests or lineage; logic lives in the console, harder to review
Incremental tables Large histories where full refresh is wasteful Cheapest at scale; only process yesterday's partition each run

For anything beyond a couple of models, dbt with incremental materializations is the pattern that scales: each run processes only the newest daily partition, models are tested, and the lineage from raw export to RFM mart is documented and reviewable. That auditability matters more than it looks — when a stakeholder questions a number, "here is the model and the test that guards it" is a far better answer than "the dashboard says so."

Common Dead Ends to Avoid

Frequently Asked Questions

Is the GA4 BigQuery export free?
The GA4 to BigQuery export itself is free for standard properties, including the daily batch export and the streaming (intraday) export. You pay normal BigQuery storage and query costs on the data once it lands. Daily export volume is capped on standard properties; Analytics 360 raises the limit and adds a freshness SLA.
What is the difference between user_id and user_pseudo_id in GA4 BigQuery?
user_pseudo_id is the device or client identifier derived from the GA client_id cookie — it is always present but resets when a user clears cookies or switches devices. user_id is the stable identifier you explicitly set when a user logs in. Customer intelligence keys on user_id when it exists and falls back to user_pseudo_id otherwise; stitching the two is the core of identity resolution.
How do you get a single event parameter value out of the GA4 BigQuery export?
event_params is a repeated RECORD, so you UNNEST it and pull the typed value. For example, to read the session id: (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id'). The value lives in one of string_value, int_value, float_value, or double_value depending on the parameter, so you select the matching field.
How do you define a session in the GA4 BigQuery export?
A session is the combination of user_pseudo_id and the ga_session_id event parameter. Concatenating those two gives a globally unique session key you can group by. GA4 has no session table — you reconstruct sessions from events, which is why a sessionization staging model is the first thing most pipelines build.
What is RFM segmentation and how does it map to GA4 data?
RFM scores each customer on Recency (days since last purchase), Frequency (number of purchases), and Monetary value (total revenue). In GA4 BigQuery you derive all three from purchase events: recency from the latest event_timestamp, frequency from the purchase event count, and monetary from the summed purchase value. Scoring each dimension into quintiles produces actionable segments like champions, at-risk, and lapsed.
Why use BigQuery instead of the GA4 reporting interface for customer intelligence?
The GA4 interface is built for aggregate reporting and applies sampling, cardinality limits, and a fixed set of dimensions. The BigQuery export gives you raw, unsampled, event-level data you can join to CRM, orders, and product tables. Customer intelligence — per-user LTV, identity stitching, custom cohorts — needs that row-level access, which the reporting UI cannot provide.

Building a customer-intelligence stack?

I run a free community for people building real data and AI pipelines — warehouse modeling, analytics, and AI-augmented workflows. No course, no paywall, just practitioners shipping.

Join the free AI community →

Related reading: AI in Enterprise Data Work — What Actually Ships — the patterns that deliver value versus the ones that stall. AI as an Operations Layer — scheduled analysis and DataOps opportunity discovery. LLMs Explained — why private, first-party data is where enterprise AI gets interesting. For the hardware side of running AI locally, see Blendlogic Tech.

Get the build notes

Real AI experiments — what shipped, what failed, and the setups behind them. Straight to your inbox.

No spam, unsubscribe anytime.

More in AI & Building →