GA4 BigQuery export: the gotchas nobody warns you about

The GA4 BigQuery export is the best thing about GA4, and the most misunderstood. It is not a copy of your reports: it is raw event data with its own delays, schema logic and definitions. Here are the gotchas that cost teams the most time, from a team that queries this export every day.

The short answer

Expect three surprises: your BigQuery numbers will not match the GA4 interface (by design, not by bug), your “yesterday” data is not final for up to 72 hours, and everything useful lives inside nested fields that punish naive queries with wrong results and big bills. All three are manageable once you know the rules.

Gotcha 1: the numbers will never match the interface

The GA4 UI estimates users with HyperLogLog approximation, applies its own channel grouping, and (with consent mode) fills gaps with modeled data. The export contains none of that: raw observed events, exact counts, no modeling, no default channel groups. Comparing COUNT(DISTINCT user_pseudo_id) against the UI’s Users metric and expecting equality is the most common false alarm we see. Define which source is authoritative for which metric, document it, and stop reconciling the unreconcilable.

Gotcha 2: yesterday is not final

Daily events_ tables can be rewritten as late-arriving events land, in practice up to about 72 hours after the day closes. The events_intraday_ table is a different, streaming-fed animal with its own gaps. Pipelines that read a daily table the morning after and never look back will quietly disagree with any later query. The fix is boring and works: schedule downstream jobs with a lag, or reprocess the trailing three days on every run.

Gotcha 3: sessions do not exist until you build them

The export has no session table. Sessions are assembled from event_params: ga_session_id scoped per user, session source reconstructed from collected_traffic_source on session_start or via first-event logic. Every team writes this differently, which is why two analysts on the same data produce different session counts. Write the session logic once, put it in a dataset both dashboards and ad-hoc queries share, and treat it as code: reviewed and versioned.

Gotcha 4: the schema punishes SELECT *

event_params, user_properties and items are nested repeated fields. UNNEST correctly or your joins silently multiply rows, inflating revenue in a way that looks plausible. And because BigQuery bills by bytes scanned, SELECT * across events_* is how teams turn a free export into a surprising invoice. Always filter _TABLE_SUFFIX, always select columns, and build slim derived tables for anything queried daily.

Gotcha 5: what is simply not there

Consent-declined traffic that the UI models back in, Google Ads cost data, default channel groupings, and anything from before you enabled the export: none of it is in the raw tables. If your warehouse needs cost data, that is a separate pipeline from the ads platforms; if your reporting needs modeled totals, the export alone cannot deliver them. Knowing what the export is NOT saves weeks of confused debugging.

The systematic version

Export completeness and schema health are standing checks in our Measurement Health Framework, and building the session logic, cost pipelines and slim reporting tables on top of the raw export is exactly what our Data Engineering & BigQuery service does. If your export is enabled but underused, that is the usual starting point.

FAQ

Should we use the daily or the streaming export?

Why is our BigQuery revenue higher than GA4’s?

Does the export include historical data from before we enabled it?

How much does querying the export cost?

Not sure if your data is telling the truth?