← Back to portfolio SQL Analytics Case Study preview
Stack
SQLDuckDBPythonpandas / NumPypytest
Impact
  • 25 self-contained SQL cases (funnel → RFM)
  • DuckDB — no server, no credentials, one command
  • Regression tests with deterministic invariants per case
  • Synthetic deterministic data (seed=42 + additive seed=43)
  • Live interactive report on GitHub Pages
Source View on GitHub → Updated: Sep 15, 2026

SQL Analytics Case Study

Context

A take-home format: 25 end-to-end SQL cases on a synthetic product dataset. Each case is one self-contained .sql file with the question and approach in a leading comment. No server, no credentials — a single command builds the data and a DuckDB database.

Data & Method

Data model (synthetic, seed=42, deterministic):

Entity Volume Fields
Users 20,000 signups (Jan–Jun 2024) channel, country, device, ab_variant
Events ~183k funnel events app_open → view_item → add_to_cart → checkout → purchase, ~80k sessions
Orders 928 purchases amount, product category
Subscriptions 268 conversions monthly / annual plans
Cancellations (seed=43) 98 subscription_cancellations
Refunds (seed=43) 53 refunds

Schema: data/schema.sql. Generator: data/generate_data.py. Engagement decays geometrically from signup; retention is weighted by acquisition channel. Additive tables (cases 21–25) are generated on a separate RNG stream (seed=43) so the seed-42 numbers in cases 1–20 never move.

25 cases:

# Case Technique
01 Funnel conversion cumulative counts, LAG / FIRST_VALUE
02 N-day retention by cohort DATE_TRUNC('month', signup_date)
03 Rolling 30-day retention EXISTS subqueries per window
04 DAU / MAU / stickiness trailing-28d range join
05 LTV by cohort left join + COALESCE for zero-revenue
06 Top-N categories per country ROW_NUMBER() OVER (PARTITION BY ...)
07 Cumulative revenue SUM() ... UNBOUNDED PRECEDING
08 Longest active-day streak gaps-and-islands (row_number → island key)
09 A/B conversion by variant in-SQL z-test + p-value (Abramowitz–Stegun)
10 Revenue attribution first-touch vs lifetime, correlated subquery
11 7-day moving average of DAU AVG() OVER (... ROWS BETWEEN 6 PRECEDING ...)
12 Top-2 revenue users per country QUALIFY
13 Monthly revenue by category PIVOT long → wide
14 Subscription MRR recursive CTE (billing rows per subscription)
15 Order amount distribution MEDIAN, QUANTILE_CONT (p90/p99)
16 Sessionization + session depth gaps-and-islands on timestamps, validation vs ground truth
17 Weekly lifecycle (new/returning/resurrecting/dormant) state transitions, LAG/LEAD
18 Cohort revenue retention (triangle) months-since-signup, % of period-0
19 Repeat purchase & time between orders LAG within user
20 RFM segmentation NTILE quintiles, segment-score rules
21 Subscription churn (logo & MRR) monthly churn, FILTER aggregates
22 Refunds & net revenue left join, gross-vs-net
23 Pareto / revenue concentration NTILE(10), cumulative-share curve
24 Daily revenue anomaly detection robust MAD z-score, rolling baseline
25 Purchase → subscription conversion join to subscriptions, time-to-convert

Quick start

uv run python data/generate_data.py   # data/analytics.duckdb
uv run python run.py            # list cases
uv run python run.py 1          # run case 1
uv run python run.py 9 --limit 20
uv run --extra dev pytest -q    # 41 regression tests
uv run python scripts/report.py # reports/index.html

The runner prints the case question, executes the SQL against data/analytics.duckdb, and renders the result as a table. The charted report is published to GitHub Pages automatically on every push.

Findings

Each case covers a specific window-function pattern that shows up in real product tasks. The findings are honest rather than engineered:

  • the repeat rate is just 3.5% (896 buyers, 31 repeat) — this is a one-and-done purchase engine;
  • RFM degenerates into a recency story;
  • the top decile delivers only 22% of revenue (no whales);
  • logo churn climbs to ~15%/month even as MRR compounds.

Splitting question and SQL in one file plus regression invariants makes the cases self-checking.

Impact

  • 25 self-contained SQL cases — from funnel to RFM, each with its own window pattern.
  • Sessionization with validation — a 30-min gap reproduces 80k pre-assigned sessions at 99.6% fidelity.
  • Additive data without breaking golden answers — seed=43 on a separate RNG stream.
  • DuckDB with no infrastructure — one command builds data and database.
  • Per-case regression tests — 41 tests (invariants + golden answers) keep cases.md and the code in sync.
  • Live report — GitHub Pages refreshes on every push.

Documentation

Case study

Problem

An analyst needs to show SQL skill on product tasks, but there is no production data, and textbook exercises do not demonstrate systems thinking. How do you prove SQL is a working tool rather than a set of memorised syntax?

Approach

25 end-to-end cases on a synthetic dataset (seed=42, ~183k events, 20k signups): each case is one self-contained .sql file with the question and approach in a leading comment. DuckDB builds the data and the database in one command, with no server or credentials. Regression tests with deterministic invariants protect the SQL from regressions. A second batch (churn, refunds, Pareto, anomaly detection, upsell conversion) was added on a separate RNG stream (seed=43) — the first 20 cases' numbers did not change.

Result

25 cases from funnel to RFM: sessionization validated against ground truth (99.6%), lifecycle composition, a revenue-retention triangle, an in-SQL z-test for A/B, and MAD anomaly analysis. The cases are self-checking: pytest confirms the SQL keeps returning the expected metrics after any data change. The report is published to GitHub Pages automatically.

25 SQL cases
~183k Dataset events
20k Signups
41 Regression tests

Charts

Source: github.com/NikitaBoyarkin/sql-analytics-case-study: cases/01_funnel_conversion.sql, cases/13_pivot_revenue.sql, cases/18_cohort_revenue_retention.sql, cases/14_recursive_subscription_mrr.sql — each run as-is against data/analytics.duckdb (deterministic, seed=42, regenerated via data/generate_data.py)

Purchase funnel conversion (session-level)

Unique sessions reaching each funnel step: app_open -> view_item -> add_to_cart -> checkout -> purchase. From cases/01_funnel_conversion.sql across 80,000 sessions of the synthetic dataset.

app_open: 80000 (100% от макс.) app_open 80000 view_item: 65739 (82.2% от макс.) view_item 65739 add_to_cart: 29641 (37.1% от макс.) add_to_cart 29641 checkout: 6584 (8.2% от макс.) checkout 6584 purchase: 928 (1.2% от макс.) purchase 928
Key takeaways
  • End-to-end conversion from app open to purchase is just 1.16% (928 of 80,000 sessions).
  • The biggest drop is add-to-cart -> checkout: 54.91% of carts never proceed (29,641 -> 6,584).

Revenue by product category, monthly

Order revenue by category and month (cases/13_pivot_revenue.sql, PIVOT). Revenue ramps through the year: total monthly revenue grows from $2,469.03 in January to $4,452.48 in June.

0 500 1000 1500 2024-01 — beauty: 466.84 2024-02 — beauty: 648.97 2024-03 — beauty: 611.16 2024-04 — beauty: 560.09 2024-05 — beauty: 768.52 2024-06 — beauty: 869.45 2024-01 — books: 344.01 2024-02 — books: 664.07 2024-03 — books: 508.99 2024-04 — books: 622.41 2024-05 — books: 532.49 2024-06 — books: 784.77 2024-01 — clothing: 432 2024-02 — clothing: 754.6 2024-03 — clothing: 930.18 2024-04 — clothing: 972.63 2024-05 — clothing: 876.49 2024-06 — clothing: 779.79 2024-01 — electronics: 385.19 2024-02 — electronics: 701.15 2024-03 — electronics: 1134.15 2024-04 — electronics: 981.57 2024-05 — electronics: 946.01 2024-06 — electronics: 656.84 2024-01 — home: 385.64 2024-02 — home: 758.01 2024-03 — home: 727.85 2024-04 — home: 753.31 2024-05 — home: 776.19 2024-06 — home: 717.07 2024-01 — sports: 455.35 2024-02 — sports: 521.8 2024-03 — sports: 919.15 2024-04 — sports: 718.79 2024-05 — sports: 883.96 2024-06 — sports: 644.56 2024-01 2024-02 2024-03 2024-04 2024-05 2024-06 Month Revenue, $ beauty books clothing electronics home sports
Key takeaways
  • Total revenue grew 1.8x: from $2,469.03 in January to $4,452.48 in June 2024.
  • Electronics peaks at $1,134.15 in March — the single highest category-month value.

Cohort revenue retention, % of month 0

Revenue of each signup cohort (rows) in months-since-signup M0-M2, as % of period 0 (cases/18_cohort_revenue_retention.sql). Shown is the fully observed 4 x 3 slice.

2024-01 2024-02 2024-03 2024-04 M0 M1 M2 2024-01 · M0: 100% 100% 2024-01 · M1: 85.8% 85.8% 2024-01 · M2: 19.5% 19.5% 2024-02 · M0: 100% 100% 2024-02 · M1: 80.8% 80.8% 2024-02 · M2: 15.3% 15.3% 2024-03 · M0: 100% 100% 2024-03 · M1: 67.3% 67.3% 2024-03 · M2: 12.2% 12.2% 2024-04 · M0: 100% 100% 2024-04 · M1: 87.6% 87.6% 2024-04 · M2: 10.4% 10.4% 0 100%
Key takeaways
  • Month-1 revenue retention is 67.3-87.6%, but by M2 it collapses to 10.4-19.5%: revenue effectively lives for one month after purchase.
  • The 2024-03 cohort holds revenue worst: -32.7pp already by M1 (100 -> 67.3%).

Subscription MRR growth

Monthly recurring revenue from subscriptions (cases/14_recursive_subscription_mrr.sql, recursive CTE: monthly = full amount, annual = amount/12). Active subscriptions grow from 18 to 268.

0 1000 2000 3000 mrr 2024-01 — mrr: 169.86 2024-02 — mrr: 522.08 2024-03 — mrr: 969.22 2024-04 — mrr: 1411.37 2024-05 — mrr: 1918.45 2024-06 — mrr: 2500.47 2024-01 2024-02 2024-03 2024-04 2024-05 2024-06 Month MRR, $
Key takeaways
  • MRR grew 14.7x in six months: from $169.86 in January to $2,500.47 in June 2024.
  • Active subscriptions grew from 18 to 268 — a 14.9x increase from January to June.

Connection map

Projects, posts and topics connected to this one. Hover a node to see its name; click to open.