Stats tables in Google Ads DTS silently inflate impression counts — sometimes by a factor of six — unless a click-type filter is applied. The problem does not appear as an error; queries run normally and dashboards display numbers that are wrong.
What Happens
Stats tables in Google Ads DTS contain a segments_click_type column. This column segments each row by the type of interaction that occurred: headline click, sitelink click, phone call, store direction click, URL click, and so on.
The trap is that impressions are repeated across every click type. When an ad is shown and generates data for three different click types, that impression appears three times in the table — once per click type row. If you sum impressions without accounting for this, you’re counting the same impression multiple times.
In practice, impression counts in DTS can be 3-6x higher than reality. Querying the raw stats table without this filter produces impression counts that do not match the Google Ads UI.
This isn’t an edge case. Every campaign with multiple interaction types — which is most campaigns — will have this duplication. It affects every table in the *Stats* family: CampaignBasicStats, AdGroupStats, KeywordStats, and more.
The Fix for Impressions
Filter to URL_CLICKS click type when summing impressions:
WHERE segments_click_type = 'URL_CLICKS'The URL_CLICKS row represents the canonical impression count for each date and entity. It captures the impression associated with the primary URL click interaction, which corresponds to what Google Ads UI shows.
The Catch: Clicks Need Different Handling
Do not apply the same filter to clicks. Filtering clicks by a single click type would give you only the subset of clicks from that interaction type, deflating your click count just as the unfiltered impression count was inflated.
The correct approach is asymmetric: filter impressions to URL_CLICKS, but sum clicks across all rows (or use a conditional sum):
SELECT campaign_id, -- Impressions: filter to URL_CLICKS to avoid duplication SUM(CASE WHEN segments_click_type = 'URL_CLICKS' THEN metrics_impressions ELSE 0 END) AS impressions, -- Clicks: sum across all click types SUM(metrics_clicks) AS clicks, -- Cost: also sum across all click types SUM(metrics_cost_micros) / 1000000 AS costFROM `project.dataset.p_CampaignBasicStats_CUSTOMERID`WHERE _DATA_DATE BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY) AND DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)GROUP BY campaign_idThis asymmetry is confusing enough that it catches experienced engineers. The instinct is to apply a consistent filter to all metrics, but impressions and clicks have different semantics in DTS’s data model. Impressions are replicated across click types; clicks are segmented by click type. The fix for one would break the other.
Why URL_CLICKS Specifically
The URL_CLICKS click type represents clicks on the ad’s final URL — the primary destination you configured. Other click types (sitelink clicks, call button clicks, directions clicks, etc.) represent supplementary interactions.
Google Ads UI reports impressions at the campaign/ad group/keyword level without click type segmentation — it shows you one impression count regardless of how many click types were triggered. The URL_CLICKS filter replicates this behavior by selecting the row that represents the ad being served, not the supplementary interaction rows.
You can verify this is working by comparing your query results against the Google Ads UI for a recent 7-day period. With the filter applied, the numbers should align within the expected variance range (see Ad Platform Metric Divergence for why some gap is normal).
In a dbt Model
This filter belongs in your base or intermediate model for campaign stats — not scattered across ad-hoc queries. Put it once in the transformation layer so every downstream model inherits the correct behavior:
-- models/base/google_ads/base__google_ads__campaign_stats.sqlSELECT _DATA_DATE AS date_day, campaign_id, SUM(CASE WHEN segments_click_type = 'URL_CLICKS' THEN metrics_impressions ELSE 0 END) AS impressions, SUM(metrics_clicks) AS clicks, SUM(metrics_interactions) AS interactions, SUM(metrics_cost_micros) / 1000000 AS cost, SUM(metrics_conversions) AS conversions, SUM(metrics_conversions_value) AS conversions_valueFROM `{{ source('google_ads', 'p_CampaignBasicStats_CUSTOMERID') }}`WHERE _DATA_DATE >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)GROUP BY 1, 2This handles both the ClickType trap (via the conditional impression sum) and the micros conversion (dividing cost by 1,000,000) in one place. Every mart model that references campaign stats can join this base model without worrying about either issue.
The Broader Pattern
The ClickType trap is a specific instance of a broader pattern in Google Ads data: the API (and DTS) returns segmented data by default, and aggregating segmented data without understanding the segmentation logic produces wrong results. The same caution applies whenever you see a segments_* column in a stats table — understand what it’s segmenting before you sum across it.
This is documented in Google’s own schema documentation, but in a way that’s easy to miss if you’re querying the data empirically rather than reading the schema spec first. Most teams discover it the hard way, usually when a number on a dashboard doesn’t match a manual check against the Google Ads UI.
Any DTS-based Google Ads reporting that does not apply this filter will produce inflated impression counts.