DB Olympics

Queries · q076 · VP Product · week 1, night 1

Storefront-to-order funnel for top merchants, Q4 2025

“Where do shoppers drop off between landing on the storefront and placing a paid order, and does the shape differ across our biggest merchants?”

Manager · asked the questionJonas FeldVP Product. Joined from a mobile-first retailer. Deciding the 2026 roadmap.
Analyst · planned the reportEmeka ObiCustomer-centric: follows cohorts of shoppers or merchants over time.
Engineer · wrote the SQLGraceWrites tidy, layered CTEs with descriptive names, one step per CTE, then a final select.

Fictional people at a fictional company, each played by a language-model agent. Who they are and what they are told

  1. A business event lands on the VP Product's desk

    Week 1

    We are redesigning the shopper journey for 2026. We need to understand the whole path, from the first visit to checkout, delivery, returns and the next purchase, and where shoppers drop out and why.

  2. Jonas Feld writes to Emeka Obi

    The manager, at the start of week 1

    Subject: Shopper journey end-to-end — what I need for the 2026 roadmap

    Emeka,

    We're resetting the shopper journey for 2026, and I need a clear picture of the whole path — first visit through checkout, delivery, returns, and whether people come back — so we can decide where to invest design and engineering time first. This feeds the roadmap review I'm presenting in two weeks, so the Friday memo should leave me able to say "here's where shoppers are leaking out, and here's the one or two places a fix would move the most volume."

    I know you only get a handful of report slots this week, so please prioritize in the order below — stop earlier in the list if something forces a tradeoff, rather than spreading thin across all of it.

    1. Where do we lose people between landing on the storefront and placing an order? I want to see the shape of the funnel (visit → browse/search → cart → checkout → paid order) and which step is the biggest drop-off, ideally for a few of our bigger merchants so we're not averaging away real differences.
    2. Once someone reaches checkout, what's stopping them from finishing? Is it a one-step cliff or a slow bleed across the checkout flow, and does it differ by device or by how they arrived (e.g. ad vs. direct vs. returning)?
    3. After the order is placed, how is delivery experience affecting trust? I want to understand how often deliveries run late and whether that visibly connects to returns or to people not coming back.
    4. How painful are returns, and does a return kill the relationship? I'd like a sense of how often a return leads to the shopper buying again within a reasonable window versus going quiet.
    5. Who comes back, and how soon? A simple read on repeat purchase behavior — how many first-time buyers return, and roughly how long it takes — would help me know if we're building a roadmap for acquisition or for retention.

    If you can only land three of these this week, do 1, 2 and 5 — funnel, checkout, and repeat purchase are the ones I most need going into the roadmap conversation. Delivery and returns can follow next week if we run out of nights.

    Thanks — looking forward to Friday.

    Jonas

  3. Emeka Obi plans the night

    The analyst · week 1, night 1, request 1 of 3

    Jonas asked for 5 things but explicitly said: if time-constrained, prioritize #1 (funnel), #2 (checkout drop-off), #5 (repeat purchase) — delivery/returns can wait. I'm spending all 3 slots on those tonight.

    Schema reality check before writing specs:

    • There's no FK linking carts/web_sessions directly (carts.session_token isn't a declared FK to web_sessions.anonymous_id), so "visit" and "cart" stages have to be joined approximately — I've asked the engineer to do a best-effort match by user_id and nearest time, and to flag if that join is too noisy rather than silently forcing it.
    • orders.cart_id IS a real FK, so cart→order→paid is solid.
    • For repeat purchase, I'm using orders.status progression as the "reached paid" signal (no need for payments table complexity at this grain) and defining first-time buyer / repeat purchase purely off orders.placed_at per user, which is clean and well-supported by the schema.
    • For device/channel on checkout, I'm leaning on web_sessions.user_agent (parsed for device class) and utm_source (channel), acknowledging this is an approximation since there's no hard session→cart link.

    I'm deliberately keeping report 1 to the top ~10 merchants by volume (not all 120) per Jonas's ask to see real differences rather than an averaged-away number, and keeping report 5 to cohorts with a full 90-day observation window given data ends 2025-12-31.

    Storefront-to-order funnel for top merchants, Q4 2025
    Question
    Where do shoppers drop off between landing on the storefront and placing a paid order, and does the shape differ across our biggest merchants?
    One row per
    tenant (top 10 merchants by paid order volume in Q4 2025)
    Columns
    • tenant name
    • visits (distinct web_sessions started in period)
    • carts created in period
    • orders placed in period (any status)
    • orders paid in period (status not in pending, cancelled)
    • visit → cart %
    • cart → order %
    • order → paid %
    • overall visit → paid %
  4. Grace writes the SQL

    The engineer · 1 dry run on the empty schema, then the submission

    Dry run 1 · STATUS: COMPILES (dry run on the empty schema, no data read)
    WITH paid_orders AS (
      SELECT tenant_id, COUNT(*) AS paid_order_count
      FROM orders
      WHERE placed_at >= DATE '2025-10-01' AND placed_at < DATE '2026-01-01'
        AND status NOT IN ('pending', 'cancelled')
      GROUP BY tenant_id
    ),
    top_tenants AS (
      SELECT tenant_id, paid_order_count
      FROM paid_orders
      ORDER BY paid_order_count DESC, tenant_id ASC
      LIMIT 10
    ),
    visits AS (
      SELECT tenant_id, COUNT(*) AS visit_count
      FROM web_sessions
      WHERE started_at >= DATE '2025-10-01' AND started_at < DATE '2026-01-01'
      GROUP BY tenant_id
    ),
    carts_created AS (
      SELECT tenant_id, COUNT(*) AS cart_count
      FROM carts
      WHERE created_at >= DATE '2025-10-01' AND created_at < DATE '2026-01-01'
      GROUP BY tenant_id
    ),
    orders_placed AS (
      SELECT tenant_id, COUNT(*) AS order_count
      FROM orders
      WHERE placed_at >= DATE '2025-10-01' AND placed_at < DATE '2026-01-01'
      GROUP BY tenant_id
    ),
    assembled AS (
      SELECT
        t.id AS tenant_id,
        t.name AS tenant_name,
        COALESCE(v.visit_count, 0) AS visits,
        COALESCE(c.cart_count, 0) AS carts_created,
        COALESCE(o.order_count, 0) AS orders_placed,
        tt.paid_order_count AS orders_paid
      FROM top_tenants tt
      JOIN tenants t ON t.id = tt.tenant_id
      LEFT JOIN visits v ON v.tenant_id = tt.tenant_id
      LEFT JOIN carts_created c ON c.tenant_id = tt.tenant_id
      LEFT JOIN orders_placed o ON o.tenant_id = tt.tenant_id
    )
    SELECT
      tenant_name,
      visits,
      carts_created,
      orders_placed,
      orders_paid,
      ROUND(CASE WHEN visits > 0 THEN 100.0 * carts_created / visits ELSE NULL END, 1) AS visit_to_cart_pct,
      ROUND(CASE WHEN carts_created > 0 THEN 100.0 * orders_placed / carts_created ELSE NULL END, 1) AS cart_to_order_pct,
      ROUND(CASE WHEN orders_placed > 0 THEN 100.0 * orders_paid / orders_placed ELSE NULL END, 1) AS order_to_paid_pct,
      ROUND(CASE WHEN visits > 0 THEN 100.0 * orders_paid / visits ELSE NULL END, 1) AS overall_visit_to_paid_pct
    FROM assembled
    ORDER BY overall_visit_to_paid_pct DESC NULLS LAST, tenant_name ASC

    “Query compiles cleanly with the right columns. Submitting.”

    Submitted · the SQL of the last dry run, unchanged · Submitted to tonight's batch. Your work on this request is done.

    “Submitted. The report returns one row per top-10 tenant (by Q4 2025 paid-order count) with visits, carts, orders placed, orders paid, and the four conversion percentages, ordered by overall visit→paid % descending.”

  5. The report comes back overnight

    As Emeka Obi saw it, from the small dataset the agents worked on

    tenant_name         | visits | carts_created | orders_placed | orders_paid | visit_to_cart_pct | cart_to_order_pct | order_to_paid_pct | overall_visit_to_paid_pct
    --------------------+--------+---------------+---------------+-------------+-------------------+-------------------+-------------------+--------------------------
    Brennquell Kaffee   |    672 |          1287 |          1172 |        1147 |             191.5 |              91.1 |              97.9 |                     170.7
    Spice Route Box     |    138 |           239 |           201 |         198 |             173.2 |              84.1 |              98.5 |                     143.5
    Grind Collective    |    245 |           365 |           351 |         346 |             149.0 |              96.2 |              98.6 |                     141.2
    Honeycomb Larder    |    148 |           192 |           188 |         186 |             129.7 |              97.9 |              98.9 |                     125.7
    Kibblewick          |    222 |           330 |           200 |         196 |             148.6 |              60.6 |              98.0 |                      88.3
    Studio Vrij         |    432 |           651 |           241 |         227 |             150.7 |              37.0 |              94.2 |                      52.5
    Petalcourt Beauty   |    485 |           669 |           256 |         243 |             137.9 |              38.3 |              94.9 |                      50.1
    Marrowby & Vane     |   1316 |          1818 |           662 |         632 |             138.1 |              36.4 |              95.5 |                      48.0
    Cragsley Outfitters |   2978 |          3922 |          1443 |        1385 |             131.7 |              36.8 |              96.0 |                      46.5
    Ampfeld Elektronik  |    691 |           720 |           195 |         187 |             104.2 |              27.1 |              95.9 |                      27.1
    (10 rows)
  6. What came of it

    The memo and the queries that followed; links found by title, so inferred

    Later queries that build on this report:

  7. It becomes benchmark query q076

    Changed only where the benchmark needs the same result on every system

    • spellingfetch first
    • determinismtie-breaker: tenant names are not unique; assembled has one row per tenant_id
    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision
    • determinismthe standard leaves the place of NULLs in a sort to the implementation: NULLS LAST ascending, NULLS FIRST descending, as in the reference results (4×)
    • placeholderfetch first
    The benchmark text, against the submitted SQL
    WITH paid_orders AS (  SELECT tenant_id, COUNT(*) AS paid_order_count  FROM orders  WHERE placed_at >= DATE '2025-10-01' AND placed_at < DATE '2026-01-01'    AND status NOT IN ('pending', 'cancelled')  GROUP BY tenant_id),top_tenants AS (  SELECT tenant_id, paid_order_count  FROM paid_orders  ORDER BY paid_order_count DESC, tenant_id ASC  LIMIT 10  ORDER BY paid_order_count DESC NULLS FIRST, tenant_id ASC NULLS LAST  {{fetch_first(10)}}),visits AS (  SELECT tenant_id, COUNT(*) AS visit_count  FROM web_sessions  WHERE started_at >= DATE '2025-10-01' AND started_at < DATE '2026-01-01'  GROUP BY tenant_id),carts_created AS (  SELECT tenant_id, COUNT(*) AS cart_count  FROM carts  WHERE created_at >= DATE '2025-10-01' AND created_at < DATE '2026-01-01'  GROUP BY tenant_id),orders_placed AS (  SELECT tenant_id, COUNT(*) AS order_count  FROM orders  WHERE placed_at >= DATE '2025-10-01' AND placed_at < DATE '2026-01-01'  GROUP BY tenant_id),assembled AS (  SELECT    t.id AS tenant_id,    t.name AS tenant_name,    COALESCE(v.visit_count, 0) AS visits,    COALESCE(c.cart_count, 0) AS carts_created,    COALESCE(o.order_count, 0) AS orders_placed,    tt.paid_order_count AS orders_paid  FROM top_tenants tt  JOIN tenants t ON t.id = tt.tenant_id  LEFT JOIN visits v ON v.tenant_id = tt.tenant_id  LEFT JOIN carts_created c ON c.tenant_id = tt.tenant_id  LEFT JOIN orders_placed o ON o.tenant_id = tt.tenant_id)SELECT  tenant_name,  visits,  carts_created,  orders_placed,  orders_paid,  ROUND(CASE WHEN visits > 0 THEN 100.0 * carts_created / visits ELSE NULL END, 1) AS visit_to_cart_pct,  ROUND(CASE WHEN carts_created > 0 THEN 100.0 * orders_placed / carts_created ELSE NULL END, 1) AS cart_to_order_pct,  ROUND(CASE WHEN orders_placed > 0 THEN 100.0 * orders_paid / orders_placed ELSE NULL END, 1) AS order_to_paid_pct,  ROUND(CASE WHEN visits > 0 THEN 100.0 * orders_paid / visits ELSE NULL END, 1) AS overall_visit_to_paid_pct  CASE WHEN visits > 0 THEN 100.0 * carts_created / visits ELSE NULL END AS visit_to_cart_pct,  CASE WHEN carts_created > 0 THEN 100.0 * orders_placed / carts_created ELSE NULL END AS cart_to_order_pct,  CASE WHEN orders_placed > 0 THEN 100.0 * orders_paid / orders_placed ELSE NULL END AS order_to_paid_pct,  CASE WHEN visits > 0 THEN 100.0 * orders_paid / visits ELSE NULL END AS overall_visit_to_paid_pctFROM assembledORDER BY overall_visit_to_paid_pct DESC NULLS LAST, tenant_name ASCORDER BY overall_visit_to_paid_pct DESC NULLS LAST, tenant_name ASC NULLS LAST, tenant_id NULLS LAST
    WITH paid_orders AS (
      SELECT tenant_id, COUNT(*) AS paid_order_count
      FROM orders
      WHERE placed_at >= DATE '2025-10-01' AND placed_at < DATE '2026-01-01'
        AND status NOT IN ('pending', 'cancelled')
      GROUP BY tenant_id
    ),
    top_tenants AS (
      SELECT tenant_id, paid_order_count
      FROM paid_orders
      ORDER BY paid_order_count DESC NULLS FIRST, tenant_id ASC NULLS LAST
      FETCH FIRST 10 ROWS ONLY
    ),
    visits AS (
      SELECT tenant_id, COUNT(*) AS visit_count
      FROM web_sessions
      WHERE started_at >= DATE '2025-10-01' AND started_at < DATE '2026-01-01'
      GROUP BY tenant_id
    ),
    carts_created AS (
      SELECT tenant_id, COUNT(*) AS cart_count
      FROM carts
      WHERE created_at >= DATE '2025-10-01' AND created_at < DATE '2026-01-01'
      GROUP BY tenant_id
    ),
    orders_placed AS (
      SELECT tenant_id, COUNT(*) AS order_count
      FROM orders
      WHERE placed_at >= DATE '2025-10-01' AND placed_at < DATE '2026-01-01'
      GROUP BY tenant_id
    ),
    assembled AS (
      SELECT
        t.id AS tenant_id,
        t.name AS tenant_name,
        COALESCE(v.visit_count, 0) AS visits,
        COALESCE(c.cart_count, 0) AS carts_created,
        COALESCE(o.order_count, 0) AS orders_placed,
        tt.paid_order_count AS orders_paid
      FROM top_tenants tt
      JOIN tenants t ON t.id = tt.tenant_id
      LEFT JOIN visits v ON v.tenant_id = tt.tenant_id
      LEFT JOIN carts_created c ON c.tenant_id = tt.tenant_id
      LEFT JOIN orders_placed o ON o.tenant_id = tt.tenant_id
    )
    SELECT
      tenant_name,
      visits,
      carts_created,
      orders_placed,
      orders_paid,
      CASE WHEN visits > 0 THEN 100.0 * carts_created / visits ELSE NULL END AS visit_to_cart_pct,
      CASE WHEN carts_created > 0 THEN 100.0 * orders_placed / carts_created ELSE NULL END AS cart_to_order_pct,
      CASE WHEN orders_placed > 0 THEN 100.0 * orders_paid / orders_placed ELSE NULL END AS order_to_paid_pct,
      CASE WHEN visits > 0 THEN 100.0 * orders_paid / visits ELSE NULL END AS overall_visit_to_paid_pct
    FROM assembled
    ORDER BY overall_visit_to_paid_pct DESC NULLS LAST, tenant_name ASC NULLS LAST, tenant_id NULLS LAST
    10 ms100 ms1 sHyper48.0 msDuckDB12.1 msGizmoSQL17.4 msclickhouse_brew159 msFirebolt92.4 msDataFusion143 msClickHouse111 ms
  8. Everything, as recorded

    The agents' own records, as the role play wrote them; only the personal data of the person who ran it is redacted

    Grace's whole session, 7 turns
    • The request, as Grace received it
      Report request from Emeka Obi (analyst for the VP Product):
      
      Title: Storefront-to-order funnel for top merchants, Q4 2025
      Question: Where do shoppers drop off between landing on the storefront and placing a paid order, and does the shape differ across our biggest merchants?
      Grain: one row per tenant (top 10 merchants by paid order volume in Q4 2025)
      Columns:
      - tenant name
      - visits (distinct web_sessions started in period)
      - carts created in period
      - orders placed in period (any status)
      - orders paid in period (status not in pending, cancelled)
      - visit → cart %
      - cart → order %
      - order → paid %
      - overall visit → paid %
      Details: Time window: 2025-10-01 to 2025-12-31 (orders.placed_at, carts.created_at, web_sessions.started_at all within this range). Tenant selection: rank tenants by count of orders with status not in ('pending','cancelled') and placed_at in this window; keep the top 10, ordered descending by that paid-order count. Tables: tenants (name), web_sessions (visits, started_at, tenant_id), carts (carts created, created_at, tenant_id), orders (orders placed and orders paid, placed_at, status, tenant_id). No FK links web_sessions to carts or carts to orders' session — just count each stage independently per tenant per period (don't try to force a row-level join between visits and carts). Round percentages to 1 decimal. Order final report by overall visit→paid % descending, tie-break by tenant name.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Grace calls dry_run
      WITH paid_orders AS (
        SELECT tenant_id, COUNT(*) AS paid_order_count
        FROM orders
        WHERE placed_at >= DATE '2025-10-01' AND placed_at < DATE '2026-01-01'
          AND status NOT IN ('pending', 'cancelled')
        GROUP BY tenant_id
      ),
      top_tenants AS (
        SELECT tenant_id, paid_order_count
        FROM paid_orders
        ORDER BY paid_order_count DESC, tenant_id ASC
        LIMIT 10
      ),
      visits AS (
        SELECT tenant_id, COUNT(*) AS visit_count
        FROM web_sessions
        WHERE started_at >= DATE '2025-10-01' AND started_at < DATE '2026-01-01'
        GROUP BY tenant_id
      ),
      carts_created AS (
        SELECT tenant_id, COUNT(*) AS cart_count
        FROM carts
        WHERE created_at >= DATE '2025-10-01' AND created_at < DATE '2026-01-01'
        GROUP BY tenant_id
      ),
      orders_placed AS (
        SELECT tenant_id, COUNT(*) AS order_count
        FROM orders
        WHERE placed_at >= DATE '2025-10-01' AND placed_at < DATE '2026-01-01'
        GROUP BY tenant_id
      ),
      assembled AS (
        SELECT
          t.id AS tenant_id,
          t.name AS tenant_name,
          COALESCE(v.visit_count, 0) AS visits,
          COALESCE(c.cart_count, 0) AS carts_created,
          COALESCE(o.order_count, 0) AS orders_placed,
          tt.paid_order_count AS orders_paid
        FROM top_tenants tt
        JOIN tenants t ON t.id = tt.tenant_id
        LEFT JOIN visits v ON v.tenant_id = tt.tenant_id
        LEFT JOIN carts_created c ON c.tenant_id = tt.tenant_id
        LEFT JOIN orders_placed o ON o.tenant_id = tt.tenant_id
      )
      SELECT
        tenant_name,
        visits,
        carts_created,
        orders_placed,
        orders_paid,
        ROUND(CASE WHEN visits > 0 THEN 100.0 * carts_created / visits ELSE NULL END, 1) AS visit_to_cart_pct,
        ROUND(CASE WHEN carts_created > 0 THEN 100.0 * orders_placed / carts_created ELSE NULL END, 1) AS cart_to_order_pct,
        ROUND(CASE WHEN orders_placed > 0 THEN 100.0 * orders_paid / orders_placed ELSE NULL END, 1) AS order_to_paid_pct,
        ROUND(CASE WHEN visits > 0 THEN 100.0 * orders_paid / visits ELSE NULL END, 1) AS overall_visit_to_paid_pct
      FROM assembled
      ORDER BY overall_visit_to_paid_pct DESC NULLS LAST, tenant_name ASC
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: tenant_name, visits, carts_created, orders_placed, orders_paid, visit_to_cart_pct, cart_to_order_pct, order_to_paid_pct, overall_visit_to_paid_pct
    • GraceQuery compiles cleanly with the right columns. Submitting.
    • Grace calls submit
      WITH paid_orders AS (
        SELECT tenant_id, COUNT(*) AS paid_order_count
        FROM orders
        WHERE placed_at >= DATE '2025-10-01' AND placed_at < DATE '2026-01-01'
          AND status NOT IN ('pending', 'cancelled')
        GROUP BY tenant_id
      ),
      top_tenants AS (
        SELECT tenant_id, paid_order_count
        FROM paid_orders
        ORDER BY paid_order_count DESC, tenant_id ASC
        LIMIT 10
      ),
      visits AS (
        SELECT tenant_id, COUNT(*) AS visit_count
        FROM web_sessions
        WHERE started_at >= DATE '2025-10-01' AND started_at < DATE '2026-01-01'
        GROUP BY tenant_id
      ),
      carts_created AS (
        SELECT tenant_id, COUNT(*) AS cart_count
        FROM carts
        WHERE created_at >= DATE '2025-10-01' AND created_at < DATE '2026-01-01'
        GROUP BY tenant_id
      ),
      orders_placed AS (
        SELECT tenant_id, COUNT(*) AS order_count
        FROM orders
        WHERE placed_at >= DATE '2025-10-01' AND placed_at < DATE '2026-01-01'
        GROUP BY tenant_id
      ),
      assembled AS (
        SELECT
          t.id AS tenant_id,
          t.name AS tenant_name,
          COALESCE(v.visit_count, 0) AS visits,
          COALESCE(c.cart_count, 0) AS carts_created,
          COALESCE(o.order_count, 0) AS orders_placed,
          tt.paid_order_count AS orders_paid
        FROM top_tenants tt
        JOIN tenants t ON t.id = tt.tenant_id
        LEFT JOIN visits v ON v.tenant_id = tt.tenant_id
        LEFT JOIN carts_created c ON c.tenant_id = tt.tenant_id
        LEFT JOIN orders_placed o ON o.tenant_id = tt.tenant_id
      )
      SELECT
        tenant_name,
        visits,
        carts_created,
        orders_placed,
        orders_paid,
        ROUND(CASE WHEN visits > 0 THEN 100.0 * carts_created / visits ELSE NULL END, 1) AS visit_to_cart_pct,
        ROUND(CASE WHEN carts_created > 0 THEN 100.0 * orders_placed / carts_created ELSE NULL END, 1) AS cart_to_order_pct,
        ROUND(CASE WHEN orders_placed > 0 THEN 100.0 * orders_paid / orders_placed ELSE NULL END, 1) AS order_to_paid_pct,
        ROUND(CASE WHEN visits > 0 THEN 100.0 * orders_paid / visits ELSE NULL END, 1) AS overall_visit_to_paid_pct
      FROM assembled
      ORDER BY overall_visit_to_paid_pct DESC NULLS LAST, tenant_name ASC
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • GraceSubmitted. The report returns one row per top-10 tenant (by Q4 2025 paid-order count) with visits, carts, orders placed, orders paid, and the four conversion percentages, ordered by overall visit→paid % descending.