Pitfall #5: Misunderstanding the Intraday Table
The Trap
Section titled “The Trap”You’re building a dashboard that needs “today’s” data. You query events_* with today’s date in the _TABLE_SUFFIX filter and get… nothing. Or you get data, but it’s from yesterday. So you dig around, discover events_intraday_*, and start querying that instead. Problem solved, right? Until tomorrow, when someone notices your numbers for yesterday are either missing events or double-counting them. You’ve walked into one of GA4’s quieter traps.
Why It Happens
Section titled “Why It Happens”GA4’s BigQuery export actually produces two sets of tables, and they have a lifecycle most people never read about:
-
events_intraday_YYYYMMDD— Created during the day. Contains events as they stream in, refreshed multiple times per hour. This is your “live-ish” data. It’s fresh but incomplete. -
events_YYYYMMDD— Created once Google finishes processing the full day’s data. This is the final, complete table for that date.
Here’s the critical part: when events_20260325 is finalized, events_intraday_20260325 gets deleted. The intraday table is temporary. It exists only while the day is still being processed.
The timing of this handoff is where things get messy. Google doesn’t guarantee when the daily table will appear. It’s usually within 24-72 hours after the day ends, but I’ve seen it take longer. During that gap, the intraday table is your only source for that date. After the handoff, it’s gone and the daily table is your only source.
Two common mistakes:
- Querying only
events_*and wondering why “today” has no data (because the daily table for today doesn’t exist yet — the intraday table does) - Querying both
events_*andevents_intraday_*with a UNION ALL and accidentally double-counting the overlap period where both exist for the same date
The Fix
Section titled “The Fix”The cleanest pattern is a query that checks both tables but avoids overlap. The logic: for any given date, use the daily table if it exists; fall back to intraday only if the daily table doesn’t exist yet.
SELECT * FROM ( -- Final daily tables SELECT event_timestamp, event_name, user_pseudo_id, event_params, 'daily' AS source_table FROM `your_project.analytics_123456789.events_*` WHERE _TABLE_SUFFIX BETWEEN '20260320' AND '20260325'
UNION ALL
-- Intraday for dates not yet finalized SELECT event_timestamp, event_name, user_pseudo_id, event_params, 'intraday' AS source_table FROM `your_project.analytics_123456789.events_intraday_*` WHERE _TABLE_SUFFIX BETWEEN '20260320' AND '20260325' AND _TABLE_SUFFIX NOT IN ( SELECT DISTINCT _TABLE_SUFFIX FROM `your_project.analytics_123456789.events_*` WHERE _TABLE_SUFFIX BETWEEN '20260320' AND '20260325' ))The subquery in the NOT IN clause ensures you only pull intraday data for dates where no finalized daily table exists. Once the daily table lands, the intraday rows for that date are excluded automatically.
For scheduled pipelines, the more robust approach is to check INFORMATION_SCHEMA.TABLES for table existence before deciding which to query. But for ad-hoc analysis, the pattern above is reliable and readable.
One more thing: if you’re building materialized models (say, in dbt or Dataform), design your incremental logic around this handoff. Process intraday data provisionally, then reprocess once the daily table drops. The daily table is authoritative — it may contain events that the intraday table missed, and it applies final processing that intraday doesn’t.
How to Check If You Have This Problem
Section titled “How to Check If You Have This Problem”Run this to compare row counts between the intraday and daily table for the most recent date where both exist. If both return rows for the same date, you need the deduplication pattern:
SELECT 'daily' AS table_type, _TABLE_SUFFIX AS date_shard, COUNT(*) AS row_countFROM `your_project.analytics_123456789.events_*`WHERE _TABLE_SUFFIX >= FORMAT_DATE( '%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY) )GROUP BY _TABLE_SUFFIX
UNION ALL
SELECT 'intraday' AS table_type, _TABLE_SUFFIX AS date_shard, COUNT(*) AS row_countFROM `your_project.analytics_123456789.events_intraday_*`WHERE _TABLE_SUFFIX >= FORMAT_DATE( '%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY) )GROUP BY _TABLE_SUFFIX
ORDER BY date_shard, table_typeLook at the output. For recent dates, you should see either a daily row or an intraday row — not both. If you see both for the same date_shard, that’s the overlap window. Any query spanning that period without the deduplication logic above is double-counting.