Migrate the GA4 Export Dataset to Another GCP Project
Sometimes the GA4 BigQuery export ends up in the wrong place - an agency-managed project you want under internal control, a suboptimal cloud region, a sandbox that is being decommissioned, or a setup you want consolidated for governance reasons. This guide moves the export (including its history) to a new GCP project and gets GA4Dataform running there.
On a high level, the migration needs two things: repoint the GA4-BigQuery link, and transfer the historical tables. Then reinstall GA4Dataform in the new project.
This guide is based on our article How to Migrate Your GA4 BigQuery Export to Another GCP Project, which walks through the options in more detail.
Before you start: access you need
- BigQuery Data Viewer on the source dataset (the tables you are copying)
- BigQuery Data Editor and BigQuery Job User on the target project (where the data is going)
- Editor access to the GA4 property (to change the BigQuery link)
Step 1: Repoint the GA4-BigQuery link
- In GA4, go to Admin → Product links → BigQuery links.
- Delete the existing link to the old project.
- Create a new link and select the new GCP project, with the same location (region) as the old dataset.
Create the new link right after deleting the old one. If you miss a day's export, that day's data is lost for good - with Streaming enabled you might salvage part of it, but don't rely on that.
From the next daily export, GA4 creates a dataset named analytics_<property_id> in the new project and writes new events_* tables there. Deleting a link only stops future exports - nothing is deleted from the old project.
Step 2: Copy the historical data
The GA4 export is date-sharded (one events_YYYYMMDD table per day), which rules out manually copying tables in the BigQuery UI - good luck clicking through hundreds of shards. Pick one of these instead:
Option A: bq cp loop in Cloud Shell
bq cp copies one table at a time, so wrap it in a loop over the date shards:
#!/bin/bash
SOURCE_PROJECT="source-project-id"
SOURCE_DATASET="analytics_123456789"
TARGET_PROJECT="target-project-id"
TARGET_DATASET="analytics_123456789"
START_DATE="20240322"
END_DATE="20240507"
current_date=${START_DATE}
while [ "$current_date" -le "$END_DATE" ]; do
echo "Copying events_${current_date}"
bq cp -f --project_id=${SOURCE_PROJECT} \
${SOURCE_PROJECT}:${SOURCE_DATASET}.events_${current_date} \
${TARGET_PROJECT}:${TARGET_DATASET}.events_${current_date}
current_date=$(date -I -d "$current_date + 1 day" | sed 's/-//g')
done
Adjust the first six variables. The script covers the events_* shards; extend it if you also want other sharded tables from the export.
Option B: SQL CREATE TABLE COPY loop
The same looping logic in pure SQL - no Cloud Shell needed, run it straight in the BigQuery console:
DECLARE start_date DATE DEFAULT DATE('2024-05-01');
DECLARE end_date DATE DEFAULT DATE('2024-05-15');
DECLARE source_project STRING DEFAULT('source-project');
DECLARE source_dataset STRING DEFAULT('analytics_123456789');
DECLARE target_project STRING DEFAULT('target-project');
DECLARE target_dataset STRING DEFAULT('analytics_123456789');
DECLARE daterange ARRAY<DATE> DEFAULT GENERATE_DATE_ARRAY(
start_date, end_date, INTERVAL 1 DAY
);
DECLARE statement STRING DEFAULT(
'CREATE OR REPLACE TABLE ' ||
'`' || target_project || '.' || target_dataset || '.' || 'events_{suffix}` ' ||
'COPY ' ||
'`' || source_project || '.' || source_dataset || '.' || 'events_{suffix}`;'
);
DECLARE i INT64 DEFAULT 0;
DECLARE date DATE;
DECLARE suffix STRING;
DECLARE query STRING;
LOOP
SET i = i + 1;
IF(i > ARRAY_LENGTH(daterange)) THEN LEAVE;
END IF;
SET date = daterange[ORDINAL(i)];
SET suffix = FORMAT_DATE('%Y%m%d', date);
SET query = REPLACE(statement, '{suffix}', suffix);
EXECUTE IMMEDIATE query;
END LOOP;
Whichever option you pick
- Don't copy
events_intraday_*tables. They are transient; the new export produces its own. - Don't overwrite newer shards. If the new export already produced
events_YYYYMMDDtables, keep those and only copy the older ones. - Keep the region consistent. The copied dataset must end up in the region you plan to run GA4Dataform in.
Step 3: Reinstall GA4Dataform in the new project
Run the installer against the new project (see the Quick Start Guide), or set up the Community version manually. Re-apply your custom configuration from the old installation - everything under definitions/custom/ and includes/custom/.
Then rebuild the output tables over the full history (see Rebuild Tables).
Step 4: Validate and clean up
Before deleting anything, compare the two projects:
-- run in both projects and compare
SELECT _TABLE_SUFFIX AS day, COUNT(*) AS events
FROM `analytics_<property_id>.events_*`
GROUP BY day
ORDER BY day;
When the day-by-day counts match, you can delete the old dataset and the old GA4Dataform installation (see Delete Dataform repository) to stop storage costs.
If your goal is only to keep outputs in a different project than the export, you don't need to migrate anything - Premium's Cross-Project Setup reads the export from one project and writes outputs to another.