Export the query table from the Search Console Performance report, sum the impressions column, and compare it to the total in the chart above. The sum will be smaller — often dramatically smaller. Export the pages table instead and the sum can come out larger than the same total. Neither is a bug, and neither is sampling in the statistical sense.
Those two gaps come from two different mechanisms: query anonymization on one side, and a change in how impressions are counted when you group by page on the other. If you build reports, dashboards, or anomaly alerts on Search Console data without knowing which mechanism applies to which number, you will eventually explain a phantom traffic drop to a stakeholder.
Why do the query rows sum to less than the total?
Google removes rare queries from the Performance report. The Search Console documentation calls these anonymized queries — queries issued by very few users are withheld so that individual searchers cannot be identified, and Google has stated that these queries are excluded from the query dimension while their clicks and impressions still count toward the totals.
The practical consequences:
- The gap is not a fixed percentage. It depends on how much of your demand is long-tail. A site with a few thousand head-term impressions per day may see the query table account for most of its total. A large publisher or a programmatic site with millions of near-unique queries can see the majority of impressions live in the anonymized bucket.
- The gap grows as you slice. Filter to one country, one device, and one page, and the query rows that survive anonymization shrink faster than the total does. Deep segments are where the discrepancy looks most alarming and matters least.
- You cannot recover the missing queries from the API. Same data, same restriction. The API is not a back door to unanonymized rows.
- It affects trend lines, not just totals. If your long-tail share shifts — say you launch thousands of thin pages, or a core update reshuffles the tail — the ratio between "sum of query rows" and "total impressions" moves even if nothing about your reporting changed.
So treat that ratio as a metric in its own right. Track unaccounted impressions = total − sum of query rows, as a share of total. When it moves sharply, your query-level reports have changed meaning even if the headline numbers look stable.
Why can page rows sum to more than the total?
Because the counting rule changes. Google documents two aggregation modes for impressions: aggregated by property and aggregated by page. The default view — and the query dimension — aggregates by property. When you group by page, or filter to a specific page, aggregation switches to by-page.
The difference shows up whenever more than one of your URLs appears in the same result set for the same query:
| Scenario | Impressions by property | Impressions by page |
|---|---|---|
| One URL appears once | 1 | 1 on that URL |
| Two of your URLs appear for the same query | 1 | 1 on each URL (2 total) |
| Same URL appears twice (e.g. as a sitelink-style duplicate) | 1 | 1 |
Sitewide, that means the page table can legitimately exceed the property total. Sites with strong sitelinks, multiple ranking URLs per query, or heavy internal competition see the biggest divergence.
The same logic explains position. Aggregated by property, the reported position is the topmost position any of your URLs held in that result set. Aggregated by page, each URL reports its own position. So a page can show a worse average position than the property does for the identical query — you are looking at the second-best result instead of the best.
Why you cannot average average position
Average position is a ratio, not a rank. Google computes it across impressions, so every row you see is already an impression-weighted mean. Averaging those rows again with equal weight gives a number that corresponds to nothing.
-- Wrong: treats a 3-impression query and a 300,000-impression query equally
select avg(position) from gsc_daily where page = '/pricing';
-- Closer: reconstructs the impression-weighted mean
select sum(position * impressions) / sum(impressions) as avg_position
from gsc_daily where page = '/pricing';
Even the weighted version is an approximation, because each stored position is itself a rounded aggregate. And an impression-weighted mean across a page that ranks 2 for one query and 60 for two hundred others is not a useful summary of anything.
For most decisions, position distributions beat position averages. Count impressions in position buckets (1–3, 4–10, 11–20, 21+) and watch the movement between buckets. A page moving from 11 to 8 is a real event; the same site's average position drifting 0.4 usually is not.
Which limits belong to the tool and which belong to the data
Separate the two, because only one of them is fixable:
- UI table caps. The Performance report shows and exports up to 1,000 rows per dimension. That is an interface limit. Anything beyond it is retrievable through the Search Analytics API.
- API row limits. Requests return a bounded page of rows — 25,000 at most — and you paginate with
startRow. To go deeper than the API will hand you in one dimension, split the request: loop day by day, then by country or device, and union the results. Each sub-request gets its own row budget. - Anonymization. Not a limit you can page past. It is applied to the data before you see it.
- Retention. Search Console keeps roughly 16 months of Performance data. If you want history beyond that, you have to extract and store it yourself, continuously.
The small print that quietly moves numbers
Search type is a filter, not a total. The report defaults to Web. Image, Video, News, and Discover are separate. Comparing a Web-only figure to a "total organic" number from analytics guarantees a mismatch.
Data is attributed to the canonical URL. Search Console reports on Google's chosen canonical, not the URL variant a user might have seen. Duplicate-cluster changes therefore look like page-level traffic moving between URLs.
Dates are in Pacific Time. Your analytics tool is probably not. Day-level joins will smear across the boundary unless you align time zones.
The last two to three days are incomplete. Alerting on them produces a fake drop every single morning.
Domain vs URL-prefix properties see different worlds. A URL-prefix property excludes other subdomains and the other protocol. Two properties on the "same site" will disagree, correctly.
What to do with this
- Store the property-level totals and the dimension-level rows as separate tables, and never reconcile one against the other. They answer different questions with different counting rules.
- Add unaccounted impressions (total minus summed query rows) as a tracked series. Investigate changes in that ratio before you investigate changes in query-level performance.
- Replace average position in every dashboard with impression share by position bucket. Keep weighted average only where you need a single scalar, and label it as impression-weighted.
- Pull from the API day by day with pagination, tag every row with search type and property type, and normalize dates to Pacific Time on ingest.
- Exclude the trailing three days from every alert and every week-over-week comparison.
None of this makes Search Console data more precise. It makes your interpretation of it correct, which is the part that actually causes bad decisions.