The GA4 interface is convenient, but it’s also opinionated — it samples, it caps cardinality, it collapses rare values into (other), and it only keeps user-level data for a limited window. The BigQuery export sidesteps all of that. It streams your raw, unsampled, event-level data into a data warehouse you control, where you can query it with SQL and join it to anything else. This guide explains what the export gives you, how the data is structured, and when it’s worth turning on.
I’ve set up the BigQuery export on dozens of properties, and the reaction is always the same once analysts see the raw data: “I didn’t realize how much GA4 was hiding from me.” The export isn’t hiding anything — it’s the same data, just without the interface’s guardrails. That’s both its power and its responsibility.
Contents
What the BigQuery Export Actually Does
When you link a GA4 property to BigQuery, Google writes your event data into a BigQuery dataset on a schedule. Each row is a single event with all its parameters intact. Unlike the GA4 reports, this data is:
- Unsampled — every event is there, not a statistical estimate.
- Event-level — the finest possible grain, before any aggregation.
- Free of cardinality limits — no
(other)row swallowing rare values. - Yours to keep — retention is governed by BigQuery, not GA4’s user-data window.
- Joinable — combine it with CRM data, cost data, product data, anything in your warehouse.
Crucially, the BigQuery export is available on standard (free) GA4 properties, not just the paid 360 tier. In Universal Analytics, raw export was a 360-only privilege. In GA4, it’s open to everyone — which is one of the genuinely underrated upgrades of the platform.
Streaming vs Daily Export
When you set up the link, you choose how often data lands. The two options serve different needs.
| Mode | Frequency | Table | Best For |
|---|---|---|---|
| Daily | Once per day, complete batch | events_YYYYMMDD | Reporting, historical analysis — finalized data |
| Streaming | Continuous, within minutes | events_intraday_YYYYMMDD | Near-real-time dashboards, monitoring |
A common pattern is to enable both: the streaming table feeds live monitoring, while the daily table — which is finalized and de-duplicated — is what you trust for reporting. The intraday table gets replaced by the daily one once the day closes, so don’t build permanent reports on intraday data.
How the Data Is Structured
The export schema is where most newcomers stumble, because it doesn’t look like a normal flat table. Each row is one event, and several columns are nested and repeated rather than simple values. The most important quirk is the event_params field.
Instead of having one column per parameter, GA4 stores all of an event’s parameters in a single repeated event_params array. Each entry has a key and a value, and the value itself is a record with typed sub-fields (string_value, int_value, double_value, float_value). To pull out, say, the page location, you UNNEST the array and filter to the key you want.
| Column | What It Holds | Shape |
|---|---|---|
event_name | The event (e.g. page_view, purchase) | String |
event_timestamp | When it fired (microseconds) | Integer |
event_params | All event parameters | Repeated record (key + typed value) |
user_pseudo_id | Pseudonymous device/user ID | String |
user_properties | User-scoped attributes | Repeated record |
items | Products in e-commerce events | Repeated record |
device, geo, traffic_source | Context records | Nested record |
The mental model that helps: a single GA4 event row is closer to a small JSON document than a spreadsheet row. Once you’re comfortable with UNNEST on the repeated fields, the rest of the schema is straightforward.
A Taste of the SQL
Counting page views by page is the “hello world” of the GA4 export, and it shows the UNNEST pattern clearly:
SELECT
(SELECT value.string_value
FROM UNNEST(event_params)
WHERE key = 'page_location') AS page,
COUNT(*) AS views
FROM `project.analytics_XXXXXX.events_*`
WHERE event_name = 'page_view'
GROUP BY page
ORDER BY views DESC;
The events_* wildcard scans every daily table at once, and you’d normally add a _TABLE_SUFFIX filter to limit the date range — both to speed the query and to control cost. That cost point matters, which brings us to the trade-offs.
What It Costs
The export itself writes data into BigQuery, and BigQuery bills on two things: storage (how much data you keep) and query processing (how much data your queries scan). Both have a free tier, and for a small-to-mid site the export commonly stays within or near it. High-traffic properties can run up real costs, especially if analysts write unfiltered SELECT * queries over the full history.
Pricing and free-tier figures are set by Google Cloud and change over time, so confirm the current rates before committing to a heavy workload. The cost-control habits, though, are stable:
- Always filter by date with
_TABLE_SUFFIXso you scan only the days you need. - Select only the columns you use — never
SELECT *on a wide, nested table. - Set table expiration on the dataset if you don’t need indefinite history.
- Build summary tables for repeated reporting instead of re-scanning raw events every time.
When the Export Is Worth It — and When It Isn’t
| Turn It On When You Need To… | Skip It If… |
|---|---|
| Join GA4 data to CRM, cost, or product data | The standard reports answer all your questions |
| Run unsampled analysis on high-traffic data | Your traffic is low enough that sampling never bites |
| Keep event-level history beyond GA4’s retention window | You have no SQL capacity on the team |
Report on high-cardinality dimensions without (other) | You only need built-in dimensions and metrics |
| Feed a BI tool or machine-learning pipeline | You’d never look at the data outside the GA4 UI |
My honest take: turn the export on early even if you won’t query it immediately. It’s not retroactive — it only captures data from the moment you link it — so the cost of waiting is permanently losing the raw version of everything that happened before. Storage is cheap; missing history is forever.
How to Set Up the Export
- Have a Google Cloud project with billing enabled and the BigQuery API turned on.
- In GA4, go to Admin → BigQuery links and click “Link.”
- Select your Cloud project and the data location (region).
- Choose your data streams and whether to include advertising identifiers.
- Pick the export frequency — daily, streaming, or both.
- Submit. The first daily table typically appears within about a day; streaming starts almost immediately.
Once data is flowing, the raw export pairs beautifully with a deliberate reporting layer. If your aim is decision-ready output rather than raw exploration, our guide on turning analytics data into decisions covers how to shape these queries into something stakeholders act on.
Frequently Asked Questions
Is the GA4 BigQuery export free?
The export is available on standard GA4 properties at no extra GA4 cost, but BigQuery itself bills for storage and query processing. Both have a free tier that small sites often stay within. Verify current Google Cloud pricing before running heavy workloads.
Is the BigQuery export retroactive?
No. Data only flows from the moment you create the link forward. Events that happened before linking are never exported, which is why it’s worth enabling the export early even if you won’t query it right away.
What’s the difference between the daily and intraday tables?
The daily table (events_YYYYMMDD) is a finalized, de-duplicated batch written once per day and is what you trust for reporting. The intraday table (events_intraday_YYYYMMDD) updates continuously for near-real-time needs and is replaced by the daily table once the day closes.
Why are event parameters hard to query?
GA4 stores all parameters in a single repeated event_params array rather than separate columns. To read a specific parameter you UNNEST the array and filter to its key, then read the correct typed sub-field (string_value, int_value, and so on).
