Web Analytics with Google Analytics 4BigQuery export and AI-assisted analysis · Lesson 19 of 20

The GA4 BigQuery export: setup, schema and SQL you will reuse

Article · 8 min · 8 min lecture

Video lecture

The GA4 BigQuery export: setup, schema and SQL you will reuse

15 chapters · about 8 min · full transcript

Coming soon

Chapter 1 of 15

BigQuery export and SQL

  • What the export gives you
  • Options and limits
  • The schema in five ideas
  • Five reusable queries
  • Why numbers differ

The narrated lecture is in production

Every chapter is scripted and ready. Browse the chapters and read the full transcript now — the video will appear here when it’s published.

Chapters

Why the export changes what you can do

The GA4 interface is aggregated, thresholded, sometimes sampled and limited by retention. The BigQuery export gives you raw, event-level data in your own Google Cloud project: every event, with its parameters, device, geography and traffic-source fields. With it you can keep history beyond the retention window, build unsampled funnels, join GA4 with CRM, cost or order data, and schedule health checks.

Export options and limits (verify current terms)

OptionWhat it doesNotes
Daily exportOne events_YYYYMMDD table per dayStandard properties have a daily limit of 1 million events for this export; consistently exceeding it can pause the export
Streaming exportNear real-time events_intraday_YYYYMMDD tablesNo event limit; costs apply; some fields (e.g., session_traffic_source_last_click) are not in intraday tables
Fresh Daily (GA4 360)Faster, more complete daily tables for 360 properties360 only
User data exportusers_ and pseudonymous_users_ tablesUser-level attributes and predictions where available

Setup: link the property in Admin > Product links > BigQuery links, choose the Google Cloud project, the data location (choose a region that suits your data-residency obligations) and the export types. The export is not retroactive: data starts from the day you link. Link early, even if you do not query it yet. BigQuery has a free tier and a sandbox; beyond that you pay for storage and for bytes scanned by queries, so filter by date with _TABLE_SUFFIX and select only the columns you need.

The schema in five ideas

  1. One row per event. event_name, event_timestamp (microseconds, UTC), event_date (property time zone).
  2. Parameters are nested. event_params is a repeated record of key plus a value with string_value, int_value, double_value or float_value. Read one with a subquery over UNNEST(event_params).
  3. Users and sessions are derived. user_pseudo_id identifies the browser/app instance; a session is user_pseudo_id plus the ga_session_id event parameter.
  4. Traffic source has three layers. traffic_source (the user's first source), collected_traffic_source (values collected on the event, such as UTMs and click ids) and session_traffic_source_last_click (session-scoped last-click attribution matching the session acquisition reports; added in 2024, not in intraday tables).
  5. Privacy fields. privacy_info.analytics_storage and privacy_info.ads_storage show consent state (Yes, No, or null). Consent-denied rows in advanced consent mode have no user_pseudo_id. The export contains no modeled data.

Hands-on: five queries you will reuse

1. Sessions, users and purchases per day

SELECT
  event_date,
  COUNT(DISTINCT user_pseudo_id) AS users,
  COUNT(DISTINCT CONCAT(user_pseudo_id, '-',
        CAST((SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS STRING))) AS sessions,
  COUNTIF(event_name = 'purchase') AS purchases
FROM `my-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260901' AND '20260930'
  AND user_pseudo_id IS NOT NULL
GROUP BY event_date
ORDER BY event_date;

2. Revenue by session channel (last click)

SELECT
  session_traffic_source_last_click.cross_channel_campaign.default_channel_group AS channel,
  COUNT(DISTINCT ecommerce.transaction_id) AS orders,
  ROUND(SUM(ecommerce.purchase_revenue), 2) AS revenue
FROM `my-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260901' AND '20260930'
  AND event_name = 'purchase'
GROUP BY channel
ORDER BY revenue DESC;

3. Landing pages with key-event rate (first page_view of each session)

WITH pv AS (
  SELECT user_pseudo_id,
         (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS sid,
         (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location') AS page,
         event_timestamp, event_name
  FROM `my-project.analytics_123456789.events_*`
  WHERE _TABLE_SUFFIX BETWEEN '20260901' AND '20260930' AND user_pseudo_id IS NOT NULL
), sessions AS (
  SELECT user_pseudo_id, sid,
         ARRAY_AGG(IF(event_name = 'page_view', page, NULL) IGNORE NULLS ORDER BY event_timestamp LIMIT 1)[SAFE_OFFSET(0)] AS landing,
         COUNTIF(event_name IN ('purchase', 'generate_lead')) > 0 AS converted
  FROM pv GROUP BY 1, 2
)
SELECT REGEXP_REPLACE(landing, r'\?.*$', '') AS landing_page,
       COUNT(*) AS sessions,
       ROUND(AVG(IF(converted, 1, 0)) * 100, 2) AS key_event_rate_pct
FROM sessions
GROUP BY landing_page
HAVING sessions >= 100
ORDER BY sessions DESC
LIMIT 25;

4. Consent share (see module 4) and 5. Duplicate transactions (see module 2) complete the starter kit.

Why the numbers will not match the interface

Expect differences: the interface can include modeled data, applies thresholds, may use different identity (user ID, device, modeling), counts users and sessions with approximation algorithms, and attributes in its own way. Decide which source is authoritative for which purpose, document it, and stop trying to reconcile to the last unit.

Worked example: an e-commerce brand in Lahore

A Lahore home-goods brand links BigQuery, schedules a daily summary table (date, channel, sessions, orders, revenue), and joins it with its order system on transaction_id to add delivered revenue for cash-on-delivery orders. For the first time the team can report channel performance on delivered revenue, not just orders placed, and it discovers that one marketplace-referral channel has a much higher refusal rate than others.

Second worked example: a UK publisher beyond 14 months

A London publisher needs three-year content trends, but GA4 exploration retention is at most 14 months on a standard property. Because the export was linked early, the analyst builds year-over-year and three-year views of engaged sessions by content group directly from BigQuery, and feeds a Data Studio dashboard from a summary table.

Pitfalls

  • Linking the export late (no backfill).
  • Querying events_* without a _TABLE_SUFFIX filter and paying to scan years of data.
  • Counting sessions without ga_session_id, or users without handling consent-denied null ids.
  • Treating the export as "wrong" because it does not match modeled interface numbers.

Key takeaways

  • Link the BigQuery export early: it is not retroactive, and it removes sampling and retention limits for analysis.
  • Standard properties have a 1 million events/day limit on the daily export; streaming has no event limit but costs apply.
  • Read parameters with UNNEST(event_params); sessions are user_pseudo_id plus ga_session_id; the export has no modeled data.
  • Filter by _TABLE_SUFFIX, build summary tables, and label which source is authoritative for which purpose.

Check your understanding

Quick questions to lock in the lesson. They don’t count towards your certificate.

  1. A team links the BigQuery export in September and asks for last year's raw events. What happens?
  2. How do you count sessions correctly in the GA4 export?
  3. BigQuery session counts are lower than the GA4 interface for an EU-heavy site using advanced consent mode. The most likely explanation?

Put it into practice

Link the BigQuery export if it is not linked; otherwise run the lesson's first three queries for last month and document how and why they differ from the interface.

Enrol for free to save your progress

Reading is always free. Enrol to keep your place, take the final assessment and earn a verifiable certificate.