+34649324221 · Today 09:00–14:00 · 15:30–20:00

GA4 BigQuery Export Schema: Tables, Key Fields and SQL Queries That Work

· By the from Analytics teamChat on WhatsApp

How the GA4 export is organised

Each GA4 property linked to BigQuery gets one dataset called analytics_ followed by the property ID. Inside it, Google creates these tables (names as in Google's documentation, October 2026):

  • events_YYYYMMDD: one table per day from the daily export. This is the one to query.
  • events_intraday_YYYYMMDD: the current day, filled continuously if streaming export is on. Google deletes it once the daily table for that day is complete.
  • users_YYYYMMDD and pseudonymous_users_YYYYMMDD: the optional user-data export. One row per user ID or per pseudonymous ID whose data changed that day, with lifetime values, audiences and predictions.

The event tables are date-sharded rather than partitioned: one table per day. To query a date range you use the wildcard events_* and filter on _TABLE_SUFFIX. That filter is also what keeps costs down, because BigQuery only reads the tables that match.

One more thing to know before counting anything: Google keeps updating each daily table with events that arrive up to three days late, so the most recent days can still change.

The fields you will use most

event_name and event_date
The event (page_view, purchase, generate_lead...) and its date as a YYYYMMDD string in the property's time zone. event_timestamp is in microseconds, in UTC.
event_params
A repeated record of key and value pairs. Each value sits in string_value, int_value or double_value depending on its type, so you read it with a subquery on UNNEST(event_params).
user_pseudo_id
The pseudonymous ID of the browser or app instance. It is what you count as users when there is no user_id.
user_id
Your own ID for signed-in users, if you send it. It is the cleanest way to join GA4 with your CRM.
ga_session_id
Not a column but an event parameter: the timestamp in seconds when the session started. On its own it is not unique; combined with user_pseudo_id it identifies a session.
is_active_user
True if the user was active that day. Filter on it to get closer to Active users in GA4. It is only filled in the daily tables.
ecommerce and items
Revenue, transaction ID and the product array for ecommerce events.

Traffic source, collected traffic source and session last click

This is where most people get lost, because the export has three records with source and medium, and each answers a different question:

  • traffic_source: the source, medium and campaign that first acquired the user. It does not change with later visits and is not filled in intraday tables.
  • collected_traffic_source: what was collected with each event: UTM values (manual_source, manual_medium, manual_campaign_name and the rest), gclid, dclid and srsltid. It is raw data, before GA4 applies any attribution.
  • session_traffic_source_last_click: the last-click source of the session, across Google Ads and manual campaigns. Its cross_channel_campaign sub-record has source, medium and campaign_name, and there are separate sub-records for Google Ads, Search Ads 360, Display & Video 360 and Campaign Manager 360.

For session reports, session_traffic_source_last_click.cross_channel_campaign is the closest thing to session source / medium in GA4. It is still last click. The attribution GA4 applies in its own reports, such as data-driven attribution for key events, is not in the export, so if you want another model you build it yourself: how attribution models differ.

If your export is older than this record, check that it is filled in your oldest tables before using it for long comparisons.

Five GA4 BigQuery SQL queries

These queries use BigQuery standard SQL. Replace my-project.analytics_123456789 with your project and dataset, and the dates with your own range. Each one reads only the columns it needs, which is what you pay for. Google's developer documentation has more examples on its basic and advanced queries pages.

1. Users, active users and sessions

SELECT

COUNT(DISTINCT user_pseudo_id) AS users,

COUNT(DISTINCT IF(is_active_user, user_pseudo_id, NULL)) AS active_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

FROM `my-project.analytics_123456789.events_*`

WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261031'

Sessions are counted as unique pairs of user and ga_session_id over the whole range, not added up day by day: otherwise a session that crosses midnight counts twice.

2. Page views by page

SELECT

(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location') AS page,

COUNT(*) AS page_views,

COUNT(DISTINCT user_pseudo_id) AS users

FROM `my-project.analytics_123456789.events_*`

WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261031'

AND event_name = 'page_view'

GROUP BY page

ORDER BY page_views DESC

LIMIT 50

page_location is the full URL, query string included, so the same page with different UTM tags appears as separate rows. Strip the query string if you need clean paths.

3. Sessions by source and medium

SELECT

session_traffic_source_last_click.cross_channel_campaign.source AS source,

session_traffic_source_last_click.cross_channel_campaign.medium AS medium,

COUNT(DISTINCT CONCAT(user_pseudo_id, '-', CAST((SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS STRING))) AS sessions

FROM `my-project.analytics_123456789.events_*`

WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261031'

GROUP BY source, medium

ORDER BY sessions DESC

4. Key events and revenue by source and medium

SELECT

session_traffic_source_last_click.cross_channel_campaign.source AS source,

session_traffic_source_last_click.cross_channel_campaign.medium AS medium,

COUNTIF(event_name = 'generate_lead') AS leads,

COUNTIF(event_name = 'purchase') AS purchases,

SUM(IF(event_name = 'purchase', ecommerce.purchase_revenue, 0)) AS revenue

FROM `my-project.analytics_123456789.events_*`

WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261031'

GROUP BY source, medium

ORDER BY leads DESC

There is no column that flags key events: list the event names you have marked as key events in GA4.

5. Key events by channel

WITH ev AS (

SELECT event_name,

LOWER(session_traffic_source_last_click.cross_channel_campaign.medium) AS medium

FROM `my-project.analytics_123456789.events_*`

WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261031'

)

SELECT

CASE

WHEN medium IN ('cpc', 'ppc', 'paidsearch') THEN 'Paid search'

WHEN medium = 'organic' THEN 'Organic search'

WHEN medium = 'email' THEN 'Email'

WHEN medium = 'referral' THEN 'Referral'

WHEN medium IS NULL OR medium IN ('(none)', '(not set)') THEN 'Direct or unknown'

ELSE 'Other'

END AS channel,

COUNTIF(event_name = 'generate_lead') AS leads,

COUNTIF(event_name = 'purchase') AS purchases

FROM ev

GROUP BY channel

ORDER BY leads DESC

This is a simplified grouping. The export has no default channel group column, and GA4's own channel rules are longer (social sites, shopping, video and so on), so write down your version once and keep it in a shared view everyone uses.

Why BigQuery and GA4 numbers do not match

Estimated counts
GA4 estimates users and sessions with the HyperLogLog++ algorithm, while BigQuery lets you count them exactly. Google cites a precision of about 1.63% for sessions at a 95% confidence interval, so small gaps are expected.
Modelled data
With consent mode, GA4 fills gaps with behavioural and key event modelling. None of that modelled data is in the export.
Google signals
Reports can merge one person across devices with Google signals. The export does not have that information, so it shows more user_pseudo_id values than GA4 shows users.
Thresholds and (other)
Reports may hide small rows or group rare values as (other). The export has every row.
Late events and time zones
Daily tables keep changing for up to 72 hours, and event_timestamp is in UTC while event_date follows the property's time zone. Compare ranges that are at least three days old.
Attribution
GA4 applies its own attribution model in reports. The export holds collected and last-click data, not the model.
Counting method
Adding up daily session counts double-counts sessions that cross midnight. Count distinct user and session ID pairs over the whole range.

Before you build dashboards on raw events

Every query above repeats the same subquery to pull ga_session_id out of event_params. That is fine for analysis. A dashboard that runs those queries on raw events every time someone opens it reads the event tables again and again, and with on-demand pricing each read is billed.

The usual approach is a clean layer: a sessions table, a key events table and a channel mapping, rebuilt once a day, which Looker Studio dashboards or Power BI read instead of the raw export. It is cheaper, faster, and everybody works with the same definitions.

If the export is not running yet, or it has been paused, read how the daily export limit works and what you can still recover from the past.

GA4 BigQuery export schema FAQ

How do I get the session ID in the GA4 BigQuery export?

It is the ga_session_id event parameter, stored in event_params as an integer. Combine it with user_pseudo_id, or with user_id, to get a unique session.

Should I query events_ or events_intraday_ tables?

The events_YYYYMMDD tables. Google recommends them because they are the stable, complete data for each day. The intraday table is temporary and is deleted once the daily table is complete.

Why does BigQuery show more users than GA4?

Usually because of Google signals, consent mode modelling and the way GA4 estimates counts. Filtering on is_active_user gets you closer to Active users, but expect a small gap even then.

Where is the session source and medium?

In session_traffic_source_last_click.cross_channel_campaign, which holds the last-click source, medium and campaign of the session. traffic_source is the source that first acquired the user.

Is there a default channel group column?

No. The export has source, medium and campaign, and you write the grouping rules yourself in SQL.

Can I see conversions in BigQuery?

Yes, as events. There is no key event flag, so you filter by the event names you have marked as key events in GA4.

GA4 BigQuery export limitHistorical data and backfillMarketing attribution modelsBigQuery consulting

Want a clean GA4 layer in BigQuery?

Tell us what your team needs to measure. In a 30-minute call we will look at how your export is set up and tell you which tables and definitions would make it usable without writing SQL every day.

Book the 30-minute call
Reviews

What our clients say

★★★★★
«from Analytics consultants have been a great asset to us at H&M. They initially joined us to support our migration to GCP and have since helped various business teams not only migrate their reports, but also restructure their entire BI architecture, while being a front-runner in exploring how AI can be utilized within analytics.»
Bianca A.H&M
★★★★★
«Truly impressed with from analytics data visualization skills! Their professionalism and attention to detail not only met but exceeded my expectations. Working with them was a breeze, thanks to their proactive communication and politeness. Highly recommend!»
Facundo P.Puramedicina
★★★★★
«Thanks to from Analytics, we now have custom dashboards to monitor performance across all team divisions, from marketing and customer success to finance and business development. The customer success dashboard has been a complete game-changer, and our leadership team can now make informed decisions directly from clear visual data. We are forever grateful for from Analytics dashboards!»
Chryso S.Stronde