Data Engineering GSC Bulk Export Tables

BigQuery Bulk Export SQL Cookbook

Production-tested BigQuery SQL queries designed to analyze raw Google Search Console bulk export datasets without hitting the 1,000-row UI limitation.

Bulk Export Table Architecture

Google Search Console’s native BigQuery bulk export dumps your daily search performance data into two partition-enabled tables:

searchdata_site_impression

Site-level aggregated records. One row per site, query, country, device, search type, and date. Does not split records by landing URL. Best for macro brand and macro query trend analysis.

searchdata_url_impression

URL-level granular records. One row per landing page URL, query, country, device, and date. Necessary for finding cannibalization, striking distance URLs, and template audits.

Tested BigQuery SQL Queries

1. Striking Distance Opportunities (Positions 11–20)

Finds queries ranking on Page 2 (average position 11.0 to 20.0) that generated at least 500 impressions over the last 28 days. These represent the highest-leverage optimization targets.

SELECT
  query,
  url,
  SUM(impressions) AS total_impressions,
  SUM(clicks) AS total_clicks,
  ROUND(SAFE_DIVIDE(SUM(clicks), SUM(impressions)) * 100, 2) AS ctr_pct,
  ROUND(AVG(sum_position / impressions) + 1, 2) AS avg_position
FROM
  `YOUR_PROJECT_ID.searchconsole.searchdata_url_impression`
WHERE
  data_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY)
  AND query IS NOT NULL
GROUP BY
  query, url
HAVING
  avg_position BETWEEN 11.0 AND 20.0
  AND total_impressions >= 500
ORDER BY
  total_impressions DESC
LIMIT 100;

2. Query Cannibalization & Intent Conflict Detector

Identifies search queries where two or more distinct URLs from your site are competing and receiving impressions during the same 14-day window.

WITH QueryPageSummary AS (
  SELECT
    query,
    url,
    SUM(impressions) AS page_impressions,
    SUM(clicks) AS page_clicks,
    ROUND(AVG(sum_position / impressions) + 1, 2) AS page_avg_position
  FROM
    `YOUR_PROJECT_ID.searchconsole.searchdata_url_impression`
  WHERE
    data_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 14 DAY)
    AND query IS NOT NULL
  GROUP BY
    query, url
)
SELECT
  q.query,
  COUNT(DISTINCT q.url) AS competing_urls_count,
  STRING_AGG(q.url, '\n' ORDER BY q.page_impressions DESC) AS competing_urls,
  SUM(q.page_impressions) AS aggregate_impressions,
  SUM(q.page_clicks) AS aggregate_clicks
FROM
  QueryPageSummary q
GROUP BY
  q.query
HAVING
  competing_urls_count > 1
  AND aggregate_impressions >= 250
ORDER BY
  competing_urls_count DESC, aggregate_impressions DESC
LIMIT 50;

3. Year-over-Year (YoY) Search Traffic & Query Drift

Compares search demand and click volume for identical 28-day windows across two consecutive years, isolating declining queries from newly discovered keywords.

WITH CurrentPeriod AS (
  SELECT
    query,
    SUM(clicks) AS clicks_current,
    SUM(impressions) AS impressions_current
  FROM
    `YOUR_PROJECT_ID.searchconsole.searchdata_site_impression`
  WHERE
    data_date BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY) AND CURRENT_DATE()
    AND query IS NOT NULL
  GROUP BY query
),
PreviousPeriod AS (
  SELECT
    query,
    SUM(clicks) AS clicks_previous,
    SUM(impressions) AS impressions_previous
  FROM
    `YOUR_PROJECT_ID.searchconsole.searchdata_site_impression`
  WHERE
    data_date BETWEEN DATE_SUB(DATE_SUB(CURRENT_DATE(), INTERVAL 1 YEAR), INTERVAL 28 DAY)
                  AND DATE_SUB(CURRENT_DATE(), INTERVAL 1 YEAR)
    AND query IS NOT NULL
  GROUP BY query
)
SELECT
  COALESCE(c.query, p.query) AS query,
  COALESCE(c.clicks_current, 0) AS clicks_current,
  COALESCE(p.clicks_previous, 0) AS clicks_previous,
  (COALESCE(c.clicks_current, 0) - COALESCE(p.clicks_previous, 0)) AS click_difference,
  COALESCE(c.impressions_current, 0) AS impressions_current,
  COALESCE(p.impressions_previous, 0) AS impressions_previous
FROM
  CurrentPeriod c
FULL OUTER JOIN
  PreviousPeriod p ON c.query = p.query
WHERE
  COALESCE(c.impressions_current, 0) >= 100 OR COALESCE(p.impressions_previous, 0) >= 100
ORDER BY
  click_difference ASC
LIMIT 100;

4. CTR Underperformer Audit (Top 5 Ranking with Below-Average CTR)

Finds URLs ranking prominently in the top 5 positions whose click-through rate is below 5%, signaling poor title tag or meta description snippet optimization.

SELECT
  url,
  query,
  SUM(impressions) AS impressions,
  SUM(clicks) AS clicks,
  ROUND(SAFE_DIVIDE(SUM(clicks), SUM(impressions)) * 100, 2) AS ctr_pct,
  ROUND(AVG(sum_position / impressions) + 1, 2) AS avg_position
FROM
  `YOUR_PROJECT_ID.searchconsole.searchdata_url_impression`
WHERE
  data_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
  AND query IS NOT NULL
GROUP BY
  url, query
HAVING
  avg_position <= 5.0
  AND ctr_pct < 5.0
  AND impressions >= 1000
ORDER BY
  impressions DESC
LIMIT 50;

Cost Optimization: Partition Pruning Mandatory

Both export tables are partitioned by data_date. Always include a strict WHERE data_date >= ... condition in your queries. Querying without partition filtering forces BigQuery to scan your entire historical dataset, incurring unnecessary query fees.