Advanced Analytics SQL

Expert

⏱️ 30 mins read

Final schema evolution: introduces the logins table (customer_id, login_date) — user analytics needs event streams. Builds on orders (Topic 7) and customers (Topic 6).

What You'll Learn

Expert analytics SQL involves cohort analysis (grouping users by when they joined), retention analysis (% of users who return), and funnel analysis (conversion rates through a sequence of steps). These patterns appear in product analytics, growth engineering, and data science roles.

Syntax

-- Cohort: group users by first event date
-- Retention: join to later activity
-- Funnel: SUM(CASE WHEN step THEN 1 END)

Example

-- Cohort Analysis: group customers by their first-order month
WITH first_order AS (
  SELECT customer_id,
    MIN(order_date) AS first_order_date
  FROM orders
  GROUP BY customer_id
)
SELECT
  DATE_FORMAT(f.first_order_date, '%Y-%m') AS cohort_month,
  DATE_FORMAT(o.order_date, '%Y-%m')       AS order_month,
  COUNT(DISTINCT o.customer_id)            AS active_customers
FROM orders o
JOIN first_order f ON o.customer_id = f.customer_id
GROUP BY DATE_FORMAT(f.first_order_date, '%Y-%m'),
         DATE_FORMAT(o.order_date, '%Y-%m');

-- Day-1 Retention from the logins table (introduced below)
SELECT
  a.login_date                 AS cohort_day,
  COUNT(DISTINCT a.customer_id) AS day0,
  COUNT(DISTINCT b.customer_id) AS day1
FROM logins a
LEFT JOIN logins b
  ON a.customer_id = b.customer_id
  AND b.login_date = a.login_date + INTERVAL 1 DAY
GROUP BY a.login_date;

-- Funnel Analysis: the order pipeline in one pass
SELECT
  COUNT(*)                                       AS orders_placed,
  COUNT(CASE WHEN status = 'shipped' THEN 1 END) AS shipped,
  COUNT(DISTINCT CASE WHEN status = 'shipped'
    AND customer_id IN (SELECT customer_id FROM orders
                        GROUP BY customer_id HAVING COUNT(*) > 1)
    THEN customer_id END)                        AS repeat_shipped_customers
FROM orders;

Beyond the Basics

Data in play — reference tables for this topic
-- logins (introduced by this topic)
-- customer_id | login_date
-- 1 (Karl)    | 2024-01-01, 01-02, 01-03, 03-01
-- 2 (Ines)    | 2024-02-01, 02-02
-- 3 (Omar)    | 2024-04-01
-- 4 (Sofia)   | 2024-05-01
-- 5 (Maike)   | 2024-03-01, 03-02, 03-03, 03-04

-- orders (from Topic 7)
-- id | customer_id | status    | total  | order_date
-- 1  | 1           | shipped   | 240.00 | 2024-01-15
-- 2  | 2           | shipped   | 89.90  | 2024-02-03
-- 3  | 1           | pending   | 430.00 | 2024-03-11
-- 4  | 3           | shipped   | 59.50  | 2024-04-02
-- 5  | 4           | cancelled | 120.00 | 2024-05-20

Cohorts: GROUP BY twice, with a bridge

A cohort is just a GROUP BY (Topic 9) whose output becomes another query's dimension. The CTE (Topic 17) computes each customer's first-order month; joining orders back onto it tags every order with its customer's cohort. Two aggregations, one picture of retention.

-- Output of the example's cohort query:
-- cohort_month | order_month | active_customers
-- 2024-01      | 2024-01     | 1     (Karl's first order)
-- 2024-01      | 2024-03     | 1     (Karl came BACK)
-- 2024-02      | 2024-02     | 1     (Ines)
-- 2024-04      | 2024-04     | 1     (Omar)
-- 2024-05      | 2024-05     | 1     (Sofia)
-- Karl is the only returning customer — five orders, one repeat
Cohort analysis = 'group people by when they started, then group their activity by when it happened.' Every retention query is built on this skeleton.

Retention is a self-join away

Day-N retention asks: who logged in on day 0 AND day N? That is Topic 15's self-join with a twist — the join condition is on dates, not IDs alone. LEFT JOIN keeps day-0 rows whose day-1 never arrives, and counting non-NULL right rows gives the retention numerator.

-- Day-1 retention from the example, traced:
-- Karl: Jan 1 → Jan 2 exists ✓ (and Jan 2 → Jan 3 ✓)
-- Ines: Feb 1 → Feb 2 ✓
-- Maike: Mar 1 → Mar 2 ✓ (and 2→3, 3→4 ✓)
-- Omar: Apr 1 → nothing ✗   Sofia: May 1 → nothing ✗
-- Day-0 rows: 9 · Day-1 matches: 7 → 7/9 ≈ 78%
The LEFT JOIN + COUNT(b.id) pattern from Topics 15 and 8 is the whole trick — retention math is join mechanics, not a special tool.

Funnels without funnel tables: conditional counting

A funnel counts how many entities survive each stage. No analytics table required: Topic 11's conditional aggregation computes all stages in one pass. Our order funnel: 5 placed → 3 shipped → 1 shipped customer who has ordered more than once (Karl — his repeat order is still pending, which the funnel honestly reports).

-- From the example, traced on ShopCo:
-- orders_placed:            5  (all orders)
-- shipped:                  3  (1, 2, 4)
-- repeat_shipped_customers: 1  (Karl — order 1 shipped, 2 orders total)
-- Note what the funnel DOESN'T say: Karl's repeat order is pending.
One row, N stages, zero joins — the funnel is SUM/COUNT over CASE (Topic 11), and its honesty depends on how carefully you define each stage.

Common Mistakes

Not accounting for timezone differences in date grouping — all dates should be converted to the same timezone before analysis. Also: cohort sizes vary widely by month, so always show % retention alongside absolute numbers.

Interview Tips

Product analytics companies (Airbnb, Uber, Meta) ask cohort and retention questions constantly. Know how to build a retention matrix (day 0 through day 30 for each cohort). The date-minus-row-number trick for consecutive days is a must-know.

Official References

Test Yourself — 6 questions
Self-check · 0/6 answered

1. What defines a customer's cohort in the ShopCo example?

2. Tracing the cohort query on ShopCo — how many customers ever come BACK after their first order?

3. Day-1 retention is computed with…?

4. The order funnel (placed → shipped → repeat shipped customer) returns…?

5. Why did this topic introduce the logins table?

6. How should timezones be handled in cohort/retention queries?

Practice

Return the holder name and net flow (sum of amounts) of the account with the highest net flow.

⚡ Solve it in the SQL playground →

Frequently Asked Questions

How do I handle timezones in cohort and retention queries?

Store timestamps in UTC, convert to the business timezone at the query boundary — and do it ONCE, before any DATE() truncation. A session at 23:30 local is a different day in UTC, and a timezone mix will silently scatter users across the wrong cohorts.

With only five customers, is cohort analysis meaningless?

The numbers are, the mechanics aren't. Small data is the best time to learn the pattern — you can verify every row by hand. On ShopCo, you can SEE that only Karl repeated; production cohort queries compute exactly the same thing over 10M rows.