Python script: export Google Ads performance to a warehouse
A production-shaped Python pattern for pulling daily campaign performance from the Google Ads API — with the retry, paging and schema habits that keep it alive.
The Google Ads API has a learning curve, but daily campaign extraction is a solved pattern. Here's the shape we deploy, trimmed to its teachable core.
The query
Google Ads uses GAQL, a SQL-ish query language:
QUERY = """
SELECT
segments.date,
campaign.id,
campaign.name,
campaign.advertising_channel_type,
metrics.cost_micros,
metrics.impressions,
metrics.clicks,
metrics.conversions,
metrics.conversions_value
FROM campaign
WHERE segments.date DURING LAST_7_DAYS
AND campaign.status != 'REMOVED'
"""
Pull seven days on every run, not one. Ads conversions are restated for days after the click (conversion lag), so re-fetching a rolling window self-heals your history.
The extraction loop
from google.ads.googleads.client import GoogleAdsClient
def fetch_campaign_performance(client: GoogleAdsClient, customer_id: str):
service = client.get_service("GoogleAdsService")
rows = []
for batch in service.search_stream(customer_id=customer_id, query=QUERY):
for row in batch.results:
rows.append({
"date": row.segments.date,
"campaign_id": row.campaign.id,
"campaign_name": row.campaign.name,
"channel": row.campaign.advertising_channel_type.name,
"cost": row.metrics.cost_micros / 1_000_000, # micros → currency
"impressions": row.metrics.impressions,
"clicks": row.metrics.clicks,
"conversions": row.metrics.conversions,
"conversion_value": row.metrics.conversions_value,
})
return rows
Loading: idempotent or nothing
Write to the warehouse with a merge/upsert keyed on (date, campaign_id) — never a blind append. Combined with the rolling window, this makes the pipeline safely re-runnable: crash it, rerun it, run it twice — the data is identical.
MERGE INTO ads.google_campaign_daily AS t
USING staging.google_campaign_daily AS s
ON t.date = s.date AND t.campaign_id = s.campaign_id
WHEN MATCHED THEN UPDATE SET
cost = s.cost, clicks = s.clicks,
conversions = s.conversions, conversion_value = s.conversion_value
WHEN NOT MATCHED THEN INSERT ROW
Production checklist
- Retries with backoff on
InternalErrorand quota exceptions — transient failures are routine. - Alert on zero rows. Silence is a failure mode; an account that returns nothing usually means auth expired.
- Log request counts. The API has daily operation limits; know your headroom before you add ten more accounts.
- Store
cost_microsdivision in one place. Half the "our numbers are 1,000,000x off" tickets in history come from doing this twice.