runtime
55 / 88

lesson 55 Advanced product analysis · advanced

Multi-grain joins without fan-out

Combine metrics at different levels without multiplying totals.

the idea

Requests and invoices each have many rows per account. Joining raw tables creates requests × invoices and inflates sums. Aggregate each side to one row per account before joining; keep accounts with zero on either side.

tables in this lesson

The challenges use this data. Each column shows its type underneath; → marks a link to another table.

ai_accounts4 columns · 6 rows
idinteger · keyplantextregiontextsigned_up_attext
1proUS2025-01-01
2freeUS2025-01-02
3teamEU2025-01-02
4proIN2025-01-03
5freeEU2025-01-04

showing 5 of 6 rows

ai_requests7 columns · 12 rows
idinteger · keyaccount_idintegermodeltextrequested_attextinput_tokensintegeroutput_tokensintegerstatustext
11swift2025-01-01 10:00:001000200success
21reason2025-01-02 11:00:002000600success
32swift2025-01-02 12:00:005000error
43reason2025-01-02 13:00:0080002000success
53swift2025-01-03 14:00:0040001000success

showing 5 of 12 rows

invoices4 columns · 6 rows
idinteger · keyaccount_idintegerperiodtextbilled_amountreal
112025-0120
232025-0180
342025-0120
462025-0180
512025-0220

showing 5 of 6 rows

example

The idea in one query. Press Run to see what it returns; running examples never affects your progress.

example.sql
WITH usage AS(
  SELECT account_id,COUNT(*) AS request_count
  FROM ai_requests
  GROUP BY account_id
), billing AS(
  SELECT account_id,SUM(billed_amount) AS billed_amount
  FROM invoices
  GROUP BY account_id
)
SELECT
  a.id,
  COALESCE(u.request_count,0) AS request_count,
  COALESCE(b.billed_amount,0) AS billed_amount
FROM ai_accounts a
LEFT JOIN usage u ON u.account_id=a.id
LEFT JOIN billing b ON b.account_id=a.id;

Watch for: DISTINCT cannot repair inflated SUM values after a many-to-many join.

build it step by step

Each step changes one thing. Run them in order and watch the result change.

  1. Observe join fan-out. Every request pairs with every invoice for the same account. Watch request 1 appear twice.

    step-1.sql
    SELECT r.id AS request_id,i.id AS invoice_id,i.billed_amount
    FROM ai_requests r
    JOIN invoices i ON i.account_id=r.account_id
    WHERE r.account_id=1;
  2. Summarize requests first. One row per account is a safe grain for the final join.

    step-2.sql
    SELECT account_id,COUNT(*) AS request_count
    FROM ai_requests
    GROUP BY account_id;
  3. Summarize invoices first. This is also one row per account.

    step-3.sql
    SELECT account_id,SUM(billed_amount) AS billed_amount
    FROM invoices
    GROUP BY account_id;
  4. Join summaries. Neither side can multiply the other now.

    step-4.sql
    WITH usage AS(
      SELECT account_id,COUNT(*) AS request_count
      FROM ai_requests
      GROUP BY account_id
    ),billing AS(
      SELECT account_id,SUM(billed_amount) AS billed_amount
      FROM invoices
      GROUP BY account_id
    )
    SELECT
      a.id,
      COALESCE(u.request_count,0) AS request_count,
      COALESCE(b.billed_amount,0) AS billed_amount
    FROM ai_accounts a
    LEFT JOIN usage u ON u.account_id=a.id
    LEFT JOIN billing b ON b.account_id=a.id

challenge 1

Return id, request_count and billed_amount for every account across all dates. Aggregate requests and invoices separately to avoid multiplication; use zeros for missing totals.

Return idrequest_countbilled_amountany row order

challenge-1.sql
 
To master this lesson, solve both challenges without hints and pass the concept check. 3 of 3 to go.