[BigQuery Data Agents](/about/bigquery-data-agents) November 3, 2026

# Inside the GA4 BigQuery export: schema and traps

How to turn on the GA4 BigQuery export, how its event tables are built, and the traps that make an agent’s answer differ from what GA4 reports.

![](/images/65d75b0ae8d69ef95785a5d6_david-pombar.webp)

**[David Pombar](/authors-guides/david-pombar)** Swiss army knife at Trackingplan

7 min read · 1581 words

Your data agent will read the GA4 export, so its answers will be exactly as good as that export and your understanding of it. The export is not a copy of the GA4 reports. It is the raw list of events, one row each, with the parameters nested inside. Most of the wrong answers an agent gives on GA4 data come from treating that table as if it were the report.

This guide covers how to turn the export on, how its tables are built, and the six traps that make an agent’s number differ from what GA4 shows. You will need all of it to build the views in [guide 4](/guides/ga4-views-for-bigquery-agents).

![Long shelves of identical archive boxes in a storage room](/images/ga4-bigquery-export-schema-archive.jpg)

The export keeps everything, one box per day. Finding the answer is your job, or your agent’s. Image: Samuel Zeller, Wikimedia Commons, CC0

**Goal of this guide:** know what is in the GA4 export, and what is not. ** You need:** the SQL from [guide 2](/guides/sql-for-bigquery-data-agents). To link your own property, Editor access to it and Owner on a Google Cloud project.

## Turn on the export

In GA4, go to **Admin > Product links > BigQuery links > Link**. Choose the Google Cloud project, the location of the data, which data streams and events to include, and the export type. Google’s [setup page](https://support.google.com/analytics/answer/9823238) walks through each screen.

There are three export types:

-   **Daily.** One complete table per day. Standard properties can export up to 1 million events a day this way.
-   **Streaming.** Events arrive within minutes, into a table for the current day, with no event limit. Streaming has a cost on the BigQuery side.
-   **Fresh Daily.** Only for Analytics 360 properties.

The export starts from the day you link. It does not backfill history, so link it as early as you can, even before you need it.

## How the tables are built

The export creates a dataset named `analytics_<property_id>`. Inside, you get one table per day:

-   `events_YYYYMMDD`: the daily table.
-   `events_intraday_YYYYMMDD`: today’s events, filled continuously if streaming is on, and deleted once the daily table for that day is complete.

There is also an optional user-data export, with tables keyed by pseudonymous ID and by user ID. The rest of this series works on the event tables.

![Diagram of one GA4 export row: flat columns such as event\_date, event\_timestamp, event\_name, user\_pseudo\_id, device, and geo, plus nested records event\_params with keys and typed values, ecommerce, and a repeated items list.](/images/ga4-bigquery-export-schema-row.webp)

One row is one event. The flat columns are easy; the nested ones are where agents go wrong. Illustrative values.

The columns an agent uses most, from Google’s [export schema](https://support.google.com/analytics/answer/7029846):

| Column | What it holds |
| --- | --- |
| `event_date` | The date, as a `YYYYMMDD` string, in the property’s time zone |
| `event_timestamp` | When the event happened, in microseconds, in UTC |
| `event_name` | `page_view`, `purchase`, and the rest |
| `event_params` | A list of key and value pairs; each value sits in `string_value`, `int_value`, or `double_value` |
| `user_pseudo_id` | The pseudonymous ID of the browser or app instance |
| `user_id` | Your own user ID, if you send one |
| `device`, `geo` | Device category, browser, country, city |
| `traffic_source` | The source that **first acquired the user**, which never changes afterwards |
| `collected_traffic_source` | The UTMs and click IDs collected with this event |
| `session_traffic_source_last_click` | Last-click source of the session |
| `ecommerce`, `items` | Transaction totals, and the list of products in the event |

Notice that `event_date` and `event_timestamp` use different time zones. An agent that filters on one and groups by the other can move events across midnight.

## The six traps

### 1\. There are no sessions, only events

The export has no session table and no session column. A session is a unique pair of `user_pseudo_id` and the `ga_session_id` parameter, which GA4 sets on every event of the session. Sessions are not reset at midnight, so counting them day by day and adding the days counts the same session twice. Google’s [bridge-the-gap post](https://developers.google.com/analytics/blog/2023/bigquery-vs-ui) explains the method.

### 2\. `traffic_source` is the user’s first source, not the session’s

This is the trap that catches agents most often. Asked for “revenue by channel”, an agent reaches for `traffic_source.medium`, which describes how the user was first acquired, possibly months ago. For the source of the session, use `session_traffic_source_last_click` or the parameters in `collected_traffic_source`. `traffic_source` is also empty in the intraday tables.

### 3\. Values live in different columns

A parameter value is in `string_value`, `int_value`, or `double_value` depending on what was sent. `ga_session_id` is an integer, `page_location` a string, `value` usually a double. If the tracking sends a number as text one week and as a number the next, the same parameter moves column, and a query that reads only one of them loses half the data.

### 4\. `(not set)`, empty, and `NULL`

Missing values arrive as `NULL`, empty strings, or, in the GA4 interface, as `(not set)`. In the public sample, Google also replaced some values with `<Other>`. An agent that groups by a column with many missing values returns a large unlabeled bucket and presents it as a category. Our post on [(not set) in GA4](/blog/what-is-not-set-in-google-analytics) covers where those values come from.

### 5\. The export will not match the interface

Expect differences, and know why. The GA4 interface approximates users and sessions (HyperLogLog++), hides rows under thresholds, adds modeled data for visitors who declined consent, and uses Google signals. The export counts exactly and includes none of those: modeled data and Google signals are [not exported](https://support.google.com/analytics/answer/9383630). Daily tables can also be updated for up to 72 hours after the day ends. An agent’s number that is a few percent off the report is often correct.

### 6\. Attribution can’t be rebuilt exactly

GA4’s attribution models run on Google’s side and can’t be reproduced from the export. You can compute last-click by session, first-click by user, or your own rules. You can’t get “the same as the Traffic acquisition report” to the decimal. Tell your agent which one you mean; guide 7 shows how.

## The export inherits every tracking bug

There is one more trap, and no query fixes it. The export holds what GA4 received. If a release dropped `transaction_id` from the purchase event on Tuesday, or the consent banner started blocking GA4 on one market, the export has the gap too, silently. Every agent you build on it will compute a conversion rate from that gap with full confidence.

That is the part of the data quality problem that has to be solved upstream, on the hits the site sends, before they reach any table. It is what Trackingplan does: it monitors the events, properties, and destinations your site and app send, on real traffic, and alerts when one breaks. Guide 12 covers how to fold that into an agent in production.

## Practice on the public sample

The public dataset `bigquery-public-data.ga4_obfuscated_sample_ecommerce` holds the export of the Google Merchandise Store from November 1, 2020, to January 31, 2021. It is obfuscated, with placeholder values, and Google [warns](https://developers.google.com/analytics/bigquery/web-ecommerce-demo-dataset) that its internal consistency is limited, so use it to learn the structure, not to draw conclusions.

Run this to count sessions properly over December:

```sql
SELECT
  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 `bigquery-public-data.ga4_obfuscated_sample_ecommerce.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20201201' AND '20201231';
```

Then compare it with the sum of the same count run day by day. The second number is higher, and trap 1 is the reason. Google’s own [basic queries](https://developers.google.com/analytics/bigquery/basic-queries) and [advanced queries](https://developers.google.com/analytics/bigquery/advanced-queries) pages are a good next step.

## How to know you got it right

-   You can say why a session count summed by day is wrong.
-   You know which column to use for the session’s source, and why not `traffic_source`.
-   You can list four reasons the export differs from the GA4 interface.
-   You can explain why no query can fix a tracking bug that happened before the export.

## Resources

-   [Set up BigQuery Export](https://support.google.com/analytics/answer/9823238) and [BigQuery Export schema](https://support.google.com/analytics/answer/7029846), Google Analytics Help.
-   [Bridge the gap between the Google Analytics UI and BigQuery export](https://developers.google.com/analytics/blog/2023/bigquery-vs-ui), Google for Developers: why the numbers differ.
-   [GA4BigQuery](https://www.ga4bigquery.com/), by Johan van de Werken: free tutorials and a library of example queries, plus a paid membership. The reference most analysts use for GA4 SQL.
-   [Mastering GA4 With Google BigQuery](https://www.teamsimmer.com/all-courses/mastering-ga4-with-google-bigquery/), Simmer: a paid course led by Johan van de Werken, from SQL basics to advanced use cases and automation. It assumes no prior BigQuery knowledge.

## FAQ

**How do I export GA4 data to BigQuery?** In GA4, go to Admin, Product links, BigQuery links, and click Link. Choose the Google Cloud project, the data location, the streams and events to include, and the export type: daily, streaming, or both. You need Editor access to the property and Owner on the project.

**Why don’t my BigQuery numbers match the GA4 interface?** Because they are not computed the same way. The interface approximates users and sessions, applies thresholds, and includes modeled data and Google signals; the export holds the raw events, counted exactly, with none of that. Daily tables can also change for up to 72 hours. Small differences are normal.

**How do I count sessions in the GA4 BigQuery export?** There is no session table. A session is a unique pair of user\_pseudo\_id and the ga\_session\_id event parameter. Count distinct pairs over the whole period, not day by day, because a session that crosses midnight appears in two daily tables.

**Is there a limit on the GA4 BigQuery export?** Standard GA4 properties can export up to 1 million events a day with the daily export. The streaming export has no event limit. Analytics 360 properties have higher limits and an extra Fresh Daily option.

**Next:** [Build the layer an agent can read](/guides/ga4-views-for-bigquery-agents).

Part of[BigQuery data agents: a guide series from zero to expert](/guides/bigquery-data-agents)

On this page

1.  [*01*Turn on the export](#turn-on-the-export)
2.  [*02*How the tables are built](#how-the-tables-are-built)
3.  [*03*The six traps](#the-six-traps)
4.  [*04*The export inherits every tracking bug](#the-export-inherits-every-tracking-bug)
5.  [*05*Practice on the public sample](#practice-on-the-public-sample)
6.  [*06*How to know you got it right](#how-to-know-you-got-it-right)
7.  [*07*Resources](#resources)
8.  [*08*FAQ](#faq)

**Stop babysitting tags**

Trackingplan audits your real user data around the clock and flags issues before they cost you.

[Start free trial →](https://app.trackingplan.com/signup)

![](/images/65d75b0ae8d69ef95785a5d6_david-pombar.webp)

**David Pombar** Swiss army knife at Trackingplan

Read more from David, a Senior Product Strategist with 18+ years in digital product development and an atypical error detection knack.

Share Copy link [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.trackingplan.com%2Fguides%2Fga4-bigquery-export-schema) [X](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.trackingplan.com%2Fguides%2Fga4-bigquery-export-schema&text=Inside%20the%20GA4%20BigQuery%20export%3A%20schema%20and%20traps)

Read next

[All Guides →](/guides)

-   [
    
    8 minBigQuery Data AgentsNovember 3, 2026
    
    ### GA4 views for BigQuery agents: build the readable layer
    
    Turn the raw GA4 export into narrow tables a data agent can read: sessions, purchases, items, and pages, with descriptions written for the agent.
    
    by **David Pombar**](/guides/ga4-views-for-bigquery-agents)
-   [
    
    7 minBigQuery Data AgentsNovember 3, 2026
    
    ### Just enough SQL to check a BigQuery agent’s answer
    
    The SQL you need to read and audit what a BigQuery data agent writes: SELECT, WHERE, GROUP BY, JOIN, dates, and UNNEST, practiced on the GA4 sample.
    
    by **David Pombar**](/guides/sql-for-bigquery-data-agents)
-   [
    
    7 minAnalytics ImplementationFebruary 16, 2026
    
    ### What is (not set) in Google Analytics and how to fix it
    
    Learn what (not set) means in Google Analytics 4, why it shows up in landing page, source, campaign and custom dimension reports, and how to fix each cause.
    
    by **David Pombar**](/blog/what-is-not-set-in-google-analytics)

Trackingplan

## See everything. Miss nothing.

Your implementations audited around the clock with real-time, real-user data. Real-time alerts about errors or changes in your data, campaigns, pixels, privacy and consent. Let AI flag issues before they cost you.

[Start free trial →](https://app.trackingplan.com/signup) [Book a demo](/demo)
