← All posts

Every day, every time: querying the GA4 export's events_* with no _TABLE_SUFFIX filter

Without a _TABLE_SUFFIX filter, events_* reads every day you've exported, plus intraday tables with streaming on. Higher bills and totals that don't match.

Step 1 of 6 A query for the October report on the GA4 export, using the events_* wildcard.
  1. A query for the October report on the GA4 export, using the events_* wildcard.
  2. With no date filter, the wildcard reads every daily table exported: almost two years on every run.
  3. With streaming export on, the intraday tables come in too: partial data that is still changing.
  4. The cause: events_* unions every table that starts with events_. Nothing in the query limits it.
  5. The fix: filter _TABLE_SUFFIX with fixed dates. It reads October only and leaves intraday out.
  6. Check it: the estimate drops from 1.8 TiB to 41 GiB and the total now counts October only.

The GA4 BigQuery export doesn’t write one big table. It creates one table per day, events_YYYYMMDD, inside a dataset called analytics_<property_id>. To query several days at once you use a wildcard: FROM analytics_123.events_*. It’s convenient, and it’s also the easiest way to read far more data than you meant to.

What a query with no date filter looks like

You write the query for the October report with events_* and forget the date filter, or put it in the wrong place. The symptoms:

  • The editor’s estimate is huge for a monthly report: This query will process 1.8 TiB where October alone would be a few dozen GiB.
  • The totals don’t match the GA4 report for October: they include every day ever exported.
  • If streaming export is on, today’s figures also show up, and they change every time you run the query.
  • The cost repeats on every run: a scheduled query or a dashboard pays for the whole history every time.

Why events_* reads everything

A wildcard table is the union of every table in the dataset whose name starts with the prefix. With events_*, that means:

  • Every daily table exported since the link was created. In a property linked for almost two years, that’s hundreds of tables.
  • The intraday tables, if streaming export is on. Their name is events_intraday_YYYYMMDD, which also starts with events_. They hold the current day’s data as it arrives, and Google deletes each one once the daily table for that day is complete. Google’s guide recommends the daily tables when you need a stable dataset.

What limits a wildcard is a filter on the _TABLE_SUFFIX pseudo-column, the part of the name that the asterisk matches. With fixed values ('20261001', '20261028') BigQuery only reads the matching tables and bills only for those. With an expression that isn’t constant, such as a subquery, it can’t prune them in advance and reads all of them. Bear in mind too that daily tables can still receive late events for up to three days after the date.

How to find a query with no date filter

  1. Look at the editor’s estimate before running. If a query for a single month processes as much as the whole dataset, the filter isn’t working.
  2. Find events_* in your scheduled queries, views and dashboard data sources, and check that every one has a _TABLE_SUFFIX filter with fixed values.
  3. Look for filters built with subqueries or with FORMAT_DATE on a column of the table: they don’t limit the tables read.
  4. If streaming is on, check whether the query can match events_intraday_*: with events_* and no filter, it does.

How to filter each query by date

  • Always add a filter on the suffix with fixed dates:

    SELECT event_name, COUNT(*) AS events
    FROM `analytics_123.events_*`
    WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261028'
    GROUP BY event_name
    

    As the intraday suffixes start with intraday_, this range already leaves them out.

  • For relative dates, constant expressions work too, for example FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)). What doesn’t work is reading the date from another table.

  • If you need today’s data, query events_intraday_* on purpose and separately, knowing it’s still partial.

  • In scheduled queries, also set Maximum bytes billed: if the filter is lost in a change, the query fails instead of reading the whole history.

Documentation: BigQuery Export schema, Set up BigQuery Export and Querying wildcard tables.

How to check the query reads only those days

  • The estimate drops to the size of the days you asked for: in the example, from 1.8 TiB to 41 GiB.
  • The totals per day of the query match the GA4 report for closed days.
  • Running the query twice in a row gives the same result: no intraday table is coming in.

The dataset, the sizes and the dates in the example are illustrative.