4.3 Answering Business Questions with BigQuery: Worked Analyses
Key Takeaways
Turn a business question into a metric, a grain, filters, and a comparison before writing SQL.
Year-over-year growth uses DATE_TRUNC to monthly grain, LAG(revenue, 12), and SAFE_DIVIDE, which returns NULL instead of failing on a zero denominator.
Cohort retention groups customers by first-purchase month and counts distinct active customers by months since first purchase.
Funnels count distinct users at each step with COUNT(DISTINCT IF(...)), not raw events.
Validate results for join fan-out, distinct counts, time zones, and averages of averages before presenting them.
4.3 Answering Business Questions with BigQuery: Worked Analyses
Core Focus: The exam guide asks you to "analyze data to answer business questions." That means turning a vague request ("Are customers coming back?") into a precise metric, writing SQL that computes it correctly, checking the result, and delivering it in the right form. This section walks through the patterns that cover most of those requests.
Sections 4.1 and 4.2 taught the SQL building blocks. Here you combine them into analyses a stakeholder would actually ask for.
From Question to Query: A Five-Step Method
- Restate the question as a metric. "Are we losing customers?" becomes "monthly churn rate = customers active last month but not this month ÷ customers active last month."
- Fix the grain. Decide what one row of the answer represents: one customer, one day, one region-month.
- Set filters and the time window. Which orders count (completed only?), which dates, which time zone?
- Choose a comparison. A number means little alone. Compare it with last period, the same period last year, a target, or another segment.
- Validate, then present. Check totals and edge cases before anyone sees the chart.
| Business question | Metric | Grain | Comparison |
|---|---|---|---|
| "Is revenue growing?" | Sum of completed order totals | Month | Same month last year (YoY) |
| "Which products sell best in each region?" | Revenue by product | Region × product | Rank within region |
| "Do new customers come back?" | Share of a signup cohort active N months later | Cohort month × months since first order | Across cohorts |
| "Where do shoppers drop off?" | Users reaching each step | Funnel step | Step-to-step conversion |
| "Who drives most revenue?" | Cumulative revenue share | Customer | Top 20% vs. the rest |
Pattern 1: Trend and Growth (MoM and YoY)
WITH monthly AS (
SELECT
DATE_TRUNC(DATE(order_ts), MONTH) AS month,
SUM(order_total) AS revenue
FROM `sales.orders`
WHERE order_status = 'COMPLETED'
GROUP BY month
)
SELECT
month,
revenue,
LAG(revenue, 1) OVER (ORDER BY month) AS prior_month_revenue,
LAG(revenue, 12) OVER (ORDER BY month) AS same_month_last_year,
SAFE_DIVIDE(revenue - LAG(revenue, 12) OVER (ORDER BY month),
LAG(revenue, 12) OVER (ORDER BY month)) AS yoy_growth
FROM monthly
ORDER BY month;
LAG(revenue, 12) assumes every month appears. If a month can be missing, join to a calendar built with GENERATE_DATE_ARRAY first. SAFE_DIVIDE returns NULL instead of an error when the denominator is zero.
Pattern 2: Top N Within Each Group
SELECT region, product_name, SUM(revenue) AS revenue
FROM `sales.order_lines`
GROUP BY region, product_name
QUALIFY RANK() OVER (PARTITION BY region ORDER BY SUM(revenue) DESC) <= 3;
Use RANK() if ties should all appear, ROW_NUMBER() if you need exactly three rows per region.
Pattern 3: Cohort Retention
WITH firsts AS (
SELECT customer_id, DATE_TRUNC(MIN(DATE(order_ts)), MONTH) AS cohort_month
FROM `sales.orders`
GROUP BY customer_id
),
activity AS (
SELECT DISTINCT
o.customer_id,
f.cohort_month,
DATE_DIFF(DATE_TRUNC(DATE(o.order_ts), MONTH), f.cohort_month, MONTH) AS months_since_first
FROM `sales.orders` AS o
JOIN firsts AS f ON o.customer_id = f.customer_id
)
SELECT
cohort_month,
months_since_first,
COUNT(DISTINCT customer_id) AS active_customers,
SAFE_DIVIDE(COUNT(DISTINCT customer_id),
MAX(COUNT(DISTINCT customer_id)) OVER (PARTITION BY cohort_month)) AS retention_rate
FROM activity
GROUP BY cohort_month, months_since_first
ORDER BY cohort_month, months_since_first;
Month 0 always has the largest count in a cohort, so dividing by the cohort's maximum gives the retention curve (100% at month 0).
Pattern 4: Funnel Conversion
SELECT
COUNT(DISTINCT IF(event_name = 'view_item', user_id, NULL)) AS viewers,
COUNT(DISTINCT IF(event_name = 'add_to_cart', user_id, NULL)) AS cart_adders,
COUNT(DISTINCT IF(event_name = 'purchase', user_id, NULL)) AS buyers,
SAFE_DIVIDE(COUNT(DISTINCT IF(event_name = 'purchase', user_id, NULL)),
COUNT(DISTINCT IF(event_name = 'view_item', user_id, NULL))) AS view_to_purchase_rate
FROM `web.events`
WHERE event_date BETWEEN '2026-09-01' AND '2026-09-30';
Count distinct users, not events: one shopper who views ten items is still one viewer.
Pattern 5: Segmentation and Contribution (Pareto)
WITH customer_revenue AS (
SELECT customer_id, SUM(order_total) AS revenue
FROM `sales.orders`
WHERE order_status = 'COMPLETED'
GROUP BY customer_id
)
SELECT
customer_id,
revenue,
SUM(revenue) OVER (ORDER BY revenue DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
/ SUM(revenue) OVER () AS cumulative_share
FROM customer_revenue
ORDER BY revenue DESC;
Customers whose cumulative_share is at or below 0.8 are the group that drives 80% of revenue. A CASE expression on the same data turns spend into named segments ("High", "Mid", "Low") for a dashboard.
Validate Before You Present
| Check | Why it matters |
|---|---|
| Compare a total with a trusted source (finance report, last month's dashboard) | Catches wrong filters, such as including cancelled orders |
| Look for duplicates after joins | Joining orders to order lines repeats order-level amounts (fan-out) and inflates sums |
Use COUNT(DISTINCT ...) where entities, not rows, are counted | Rows can repeat per event or per line item |
| Check time zones | DATE(order_ts) uses UTC unless you pass a time zone, such as DATE(order_ts, 'America/New_York') |
Check NULL handling | AVG and COUNT(column) skip NULLs, which may or may not be what the business means |
| Avoid averaging averages | The average of regional averages is not the company average unless regions are the same size; recompute from totals |
Choosing How to Deliver the Answer
- One-off question: run the SQL in BigQuery Studio and save the query.
- Exploration with charts or statistics: use a notebook (Colab Enterprise) with BigQuery DataFrames (Section 6.1).
- Recurring question for many people: publish a dashboard in Looker or Looker Studio (Sections 6.2–6.4) on top of a summary table.
- Recurring calculation feeding other tools: materialize it with a scheduled query or Dataform (Section 9.1).
Common Exam Traps
- Fan-out after a join: Summing
order_totalafter joiningorderstoorder_itemsmultiplies each order's total by its number of items. Aggregate each table to the same grain before joining, or sum a line-level amount instead. - Dividing without protection: Plain
/raises an error when the denominator is zero;SAFE_DIVIDEreturnsNULL. - Counting rows instead of entities: "How many customers bought?" needs
COUNT(DISTINCT customer_id). - Missing periods in time series:
LAGcompares with the previous row, not the previous calendar month, if months are missing.
An analyst joins orders (one row per order, including order_total) to order_items (one row per item) and sums order_total. The result is far above the finance team's revenue figure. What went wrong?
The query should have used AVG instead of SUM for revenue
BigQuery rounds NUMERIC values upward when tables are joined on a key
SUM skips NULL values, which inflates the reported total
The join repeats each order's total once per item (fan-out)
A daily report computes conversion rate as purchases / visits, and the query fails on days with zero visits. Which expression returns NULL for those days instead of an error?
IEEE_DIVIDE(purchases, visits)
DIV(purchases, visits)
COALESCE(purchases / visits, 0)
SAFE_DIVIDE(purchases, visits)
A product manager asks: "Of the customers whose first purchase was in January, what share bought again in each following month?" Which analysis pattern answers this directly?
A page-view funnel from product view through add-to-cart to checkout
A top-3 ranking of products within each region using QUALIFY
Cohort retention by first-purchase month and months since then
A Pareto analysis of each customer's cumulative revenue share
Sections you finish are checked off in the contents.