DB Olympics

Queries · q080 · VP Product · week 1, night 2

Where orders stall after checkout: status-transition funnel, Q4 2025

“Within the post-checkout order lifecycle, is the drop-off to a paid order a single cliff at one status or a slow bleed spread across several, and which status is it?”

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 SQLBeaWrites SQL the way the company's BI tool generates it: verbose fully qualified aliases (t0, t1, ...), a subquery per measure joined back on the grain keys, COALESCE everywhere.

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 2, request 2 of 3

    Reading the three reports critically:

    Report 1 (funnel): several tenants show visit→cart conversion ABOVE 100% (up to 191.5%), which is impossible for a real per-visitor funnel. The engineer's note explains why: stages are counted independently with no row-level join, so this isn't "% of visitors who added to cart" — it's just cart_count/visit_count. The merchants with the worst over-100% ratios (Brennquell Kaffee, Spice Route Box, Grind Collective, Honeycomb Larder) read like subscription/recurring-box businesses, while the merchants with ratios close to 100-150% and a big cart→order drop (Kibblewick, Studio Vrij, Petalcourt Beauty, Marrowby & Vane, Cragsley Outfitters, Ampfeld Elektronik) read like regular one-off retail. My hypothesis: subscription merchants generate many carts/orders from recurring billing that never involve a fresh tracked web session, which would make "visits" structurally incomparable across merchant types. I want to test that before telling Jonas where the "biggest leak" is, otherwise I'd be mixing a tracking-coverage artifact with real shopper drop-off.

    For the real retail merchants, the consistent, trustworthy signal across stages is: cart→order is the dominant drop (27-61% conversion) while order→paid is consistently high (94-99%). That's the leak worth roadmapping — but I want one more clean (schema-safe) cut at where in that drop it happens, since Report 2's device/channel attempt failed.

    Report 2 (checkout by device/channel): 16,987 of ~17,158 carts (99%) landed in device=unknown/channel=unknown — the engineer flagged exactly this risk. This means the user_id-based same-day join to web_sessions essentially didn't work (most carts are likely guest/anonymous, with no user_id to match on). I'm treating device/channel-by-cart as not reliably answerable with the current schema and pivoting to schema-clean alternatives (order_status_history for stall location; cart timestamps for cliff-vs-bleed shape) rather than re-trying the same fragile join.

    Report 3 (repeat purchase): looks internally consistent — no red flags. Repeat-within-90-days sits in a 40-62% band, median days-to-second-order 32-78 days, with the two most recent cohorts showing lower medians as expected from a shorter observation window. I consider priority 5 answered; no follow-up planned tonight.

    Tonight's 3 requests: (A) test the subscription-vs-session-tracking-gap hypothesis from Report 1, (B) find exactly which status the post-checkout order stalls at using the clean orders/order_status_history join (no fragile session join needed), (C) check whether cart abandonment is a quick cliff or a slow lingering bleed using carts' own timestamps, restricted to the real-retail tenants so we're not mixing in the subscription pattern.

    Where orders stall after checkout: status-transition funnel, Q4 2025
    Question
    Within the post-checkout order lifecycle, is the drop-off to a paid order a single cliff at one status or a slow bleed spread across several, and which status is it?
    One row per
    order status
    Columns
    • status
    • orders reaching this status (count)
    • pct of total orders in window
    • median hours from order creation to reaching this status
  4. Bea 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 base_orders AS (
      SELECT o.id AS order_id, o.tenant_id, o.created_at AS order_created_at
      FROM orders o
      JOIN (
        SELECT o2.tenant_id, COUNT(*) AS order_count
        FROM orders o2
        WHERE o2.created_at >= DATE '2025-10-01' AND o2.created_at < DATE '2026-01-01'
        GROUP BY o2.tenant_id
        ORDER BY order_count DESC
        LIMIT 10
      ) top_tenants ON top_tenants.tenant_id = o.tenant_id
      WHERE o.created_at >= DATE '2025-10-01' AND o.created_at < DATE '2026-01-01'
    ),
    total_orders AS (
      SELECT COUNT(DISTINCT order_id) AS total_orders FROM base_orders
    ),
    status_list AS (
      SELECT status_name FROM (VALUES ('pending'),('confirmed'),('paid'),('partially_fulfilled'),('fulfilled'),('cancelled'),('refunded')) AS s(status_name)
    ),
    first_reach AS (
      SELECT bo.order_id, osh.to_status AS status_name, MIN(osh.created_at) AS first_reached_at
      FROM base_orders bo
      JOIN order_status_history osh ON osh.order_id = bo.order_id
      WHERE osh.to_status IN ('pending','confirmed','paid','partially_fulfilled','fulfilled','cancelled','refunded')
      GROUP BY bo.order_id, osh.to_status
    ),
    status_metrics AS (
      SELECT
        sl.status_name,
        COUNT(DISTINCT fr.order_id) AS orders_reaching_status,
        PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY (EXTRACT(EPOCH FROM (fr.first_reached_at - bo.order_created_at)) / 3600.0)) AS median_hours
      FROM status_list sl
      LEFT JOIN first_reach fr ON fr.status_name = sl.status_name
      LEFT JOIN base_orders bo ON bo.order_id = fr.order_id
      GROUP BY sl.status_name
    )
    SELECT
      sm.status_name AS status,
      COALESCE(sm.orders_reaching_status, 0) AS orders_reaching_status,
      ROUND(COALESCE(sm.orders_reaching_status, 0) * 100.0 / t.total_orders, 1) AS pct_of_total_orders,
      ROUND(sm.median_hours, 1) AS median_hours_to_status
    FROM status_metrics sm
    CROSS JOIN total_orders t
    ORDER BY pct_of_total_orders DESC, sm.status_name ASC
    Submitted · the SQL of the last dry run, unchanged · Submitted to tonight's batch. Your work on this request is done.

    “Submitted. Summary of what I did and the one deviation: - Base population: orders created 2025-10-01 to 2025-12-31 (exclusive upper bound 2026-01-01), restricted to tenants. - Tenant selection caveat: the request referenced "the same 10 tenants as the Q4 funnel report," but no tenant names were provided to me and I have no access to that companion request. I substituted the 10 tenants with the highest order counts in the Q4 2025 window as a defensible proxy, and flagged this clearly in the submit note so the analyst can correct it if the actual list differs. - Grain: one row per status (pending, confirmed, paid, partially_fulfilled, fulfilled, cancelled, refunded). - Measures: orders reaching each status (count of distinct orders with at least one order_status_history row with that to_status), pct of total base-population orders, and median hours from orders.created_at to the first time that status was reached (via PERCENTILE_CONT). - Ordering: pct descending, tie-break by status name, per spec.”

  5. The report comes back overnight

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

    status              | orders_reaching_status | pct_of_total_orders | median_hours_to_status
    --------------------+------------------------+---------------------+-----------------------
    pending             |                   4909 |               100.0 |                    0.0
    paid                |                   4810 |                98.0 |                    0.0
    fulfilled           |                   4697 |                95.7 |                   22.3
    refunded            |                    179 |                 3.6 |                  523.6
    cancelled           |                    162 |                 3.3 |                    0.1
    confirmed           |                      0 |                 0.0 |                   NULL
    partially_fulfilled |                      0 |                 0.0 |                   NULL
    (7 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:

    It builds on:

  7. It becomes benchmark query q080

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

    • spellingfetch first
    • determinismtie-breaker: tenant_id is the grouping key
    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision
    • numericsdocs/NUMERICS.md: divisor guarded with NULLIF(..., 0); it is zero when only part of the data is loaded (an empty period), where engines raise an error or return inf/nan
    • numericsdocs/NUMERICS.md: EXTRACT(EPOCH FROM (b - a)) cast to NUMERIC(18,6)
    • made explicita selected t.col gets AS col, its standard name made explicit (4×)
    • placeholderpercentile cont
    • placeholderseconds between
    • 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 base_orders AS (  SELECT o.id AS order_id, o.tenant_id, o.created_at AS order_created_at  SELECT o.id AS order_id, o.tenant_id AS tenant_id, o.created_at AS order_created_at  FROM orders o  JOIN (    SELECT o2.tenant_id, COUNT(*) AS order_count    SELECT o2.tenant_id AS tenant_id, COUNT(*) AS order_count    FROM orders o2    WHERE o2.created_at >= DATE '2025-10-01' AND o2.created_at < DATE '2026-01-01'    GROUP BY o2.tenant_id    ORDER BY order_count DESC    LIMIT 10    ORDER BY order_count DESC NULLS FIRST, o2.tenant_id NULLS LAST    {{fetch_first(10)}}  ) top_tenants ON top_tenants.tenant_id = o.tenant_id  WHERE o.created_at >= DATE '2025-10-01' AND o.created_at < DATE '2026-01-01'),total_orders AS (  SELECT COUNT(DISTINCT order_id) AS total_orders FROM base_orders),status_list AS (  SELECT status_name FROM (VALUES ('pending'),('confirmed'),('paid'),('partially_fulfilled'),('fulfilled'),('cancelled'),('refunded')) AS s(status_name)),first_reach AS (  SELECT bo.order_id, osh.to_status AS status_name, MIN(osh.created_at) AS first_reached_at  SELECT bo.order_id AS order_id, osh.to_status AS status_name, MIN(osh.created_at) AS first_reached_at  FROM base_orders bo  JOIN order_status_history osh ON osh.order_id = bo.order_id  WHERE osh.to_status IN ('pending','confirmed','paid','partially_fulfilled','fulfilled','cancelled','refunded')  GROUP BY bo.order_id, osh.to_status),status_metrics AS (  SELECT    sl.status_name,    sl.status_name AS status_name,    COUNT(DISTINCT fr.order_id) AS orders_reaching_status,    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY (EXTRACT(EPOCH FROM (fr.first_reached_at - bo.order_created_at)) / 3600.0)) AS median_hours    {{percentile_cont(0.5, ({{seconds_between(bo.order_created_at, fr.first_reached_at)}} / 3600.0))}} AS median_hours  FROM status_list sl  LEFT JOIN first_reach fr ON fr.status_name = sl.status_name  LEFT JOIN base_orders bo ON bo.order_id = fr.order_id  GROUP BY sl.status_name)SELECT  sm.status_name AS status,  COALESCE(sm.orders_reaching_status, 0) AS orders_reaching_status,  ROUND(COALESCE(sm.orders_reaching_status, 0) * 100.0 / t.total_orders, 1) AS pct_of_total_orders,  ROUND(sm.median_hours, 1) AS median_hours_to_status  COALESCE(sm.orders_reaching_status, 0) * 100.0 / NULLIF(t.total_orders, 0) AS pct_of_total_orders,  sm.median_hours AS median_hours_to_statusFROM status_metrics smCROSS JOIN total_orders tORDER BY pct_of_total_orders DESC, sm.status_name ASCORDER BY pct_of_total_orders DESC NULLS FIRST, sm.status_name ASC NULLS LAST
    WITH base_orders AS (
      SELECT o.id AS order_id, o.tenant_id AS tenant_id, o.created_at AS order_created_at
      FROM orders o
      JOIN (
        SELECT o2.tenant_id AS tenant_id, COUNT(*) AS order_count
        FROM orders o2
        WHERE o2.created_at >= DATE '2025-10-01' AND o2.created_at < DATE '2026-01-01'
        GROUP BY o2.tenant_id
        ORDER BY order_count DESC NULLS FIRST, o2.tenant_id NULLS LAST
        FETCH FIRST 10 ROWS ONLY
      ) top_tenants ON top_tenants.tenant_id = o.tenant_id
      WHERE o.created_at >= DATE '2025-10-01' AND o.created_at < DATE '2026-01-01'
    ),
    total_orders AS (
      SELECT COUNT(DISTINCT order_id) AS total_orders FROM base_orders
    ),
    status_list AS (
      SELECT status_name FROM (VALUES ('pending'),('confirmed'),('paid'),('partially_fulfilled'),('fulfilled'),('cancelled'),('refunded')) AS s(status_name)
    ),
    first_reach AS (
      SELECT bo.order_id AS order_id, osh.to_status AS status_name, MIN(osh.created_at) AS first_reached_at
      FROM base_orders bo
      JOIN order_status_history osh ON osh.order_id = bo.order_id
      WHERE osh.to_status IN ('pending','confirmed','paid','partially_fulfilled','fulfilled','cancelled','refunded')
      GROUP BY bo.order_id, osh.to_status
    ),
    status_metrics AS (
      SELECT
        sl.status_name AS status_name,
        COUNT(DISTINCT fr.order_id) AS orders_reaching_status,
        PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY (CAST(EXTRACT(EPOCH FROM (fr.first_reached_at - bo.order_created_at)) AS NUMERIC(18,6)) / 3600.0)) AS median_hours
      FROM status_list sl
      LEFT JOIN first_reach fr ON fr.status_name = sl.status_name
      LEFT JOIN base_orders bo ON bo.order_id = fr.order_id
      GROUP BY sl.status_name
    )
    SELECT
      sm.status_name AS status,
      COALESCE(sm.orders_reaching_status, 0) AS orders_reaching_status,
      COALESCE(sm.orders_reaching_status, 0) * 100.0 / NULLIF(t.total_orders, 0) AS pct_of_total_orders,
      sm.median_hours AS median_hours_to_status
    FROM status_metrics sm
    CROSS JOIN total_orders t
    ORDER BY pct_of_total_orders DESC NULLS FIRST, sm.status_name ASC NULLS LAST
    100 ms1 s10 sHyper595 msDuckDB2.80 sGizmoSQL2.36 sclickhouse_brew3.33 sFirebolt1.00 sDataFusion1.01 sClickHouse6.72 s
  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

    Bea's whole session, 6 turns
    • The request, as Bea received it
      Report request from Emeka Obi (analyst for the VP Product):
      
      Title: Where orders stall after checkout: status-transition funnel, Q4 2025
      Question: Within the post-checkout order lifecycle, is the drop-off to a paid order a single cliff at one status or a slow bleed spread across several, and which status is it?
      Grain: one row per order status
      Columns:
      - status
      - orders reaching this status (count)
      - pct of total orders in window
      - median hours from order creation to reaching this status
      Details: Base population: orders with created_at in 2025-10-01 to 2025-12-31, restricted to the same 10 tenants as the Q4 funnel report (tenant_id lookup by name as in the companion request). total_orders = count of distinct such orders. For each status in (pending, confirmed, paid, partially_fulfilled, fulfilled, cancelled, refunded), 'orders reaching this status' = count of distinct orders that have at least one row in order_status_history with to_status = that status (occurred within the same order's lifetime, no extra date filter beyond the order being in the base population). Median hours = median of (first order_status_history.created_at with that to_status for the order) minus orders.created_at, in hours, rounded to 1 decimal. pct of total orders = orders_reaching_status / total_orders * 100, rounded to 1 decimal. Order rows by pct of total orders descending, tie-break by status name.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Bea calls dry_run
      WITH base_orders AS (
        SELECT o.id AS order_id, o.tenant_id, o.created_at AS order_created_at
        FROM orders o
        JOIN (
          SELECT o2.tenant_id, COUNT(*) AS order_count
          FROM orders o2
          WHERE o2.created_at >= DATE '2025-10-01' AND o2.created_at < DATE '2026-01-01'
          GROUP BY o2.tenant_id
          ORDER BY order_count DESC
          LIMIT 10
        ) top_tenants ON top_tenants.tenant_id = o.tenant_id
        WHERE o.created_at >= DATE '2025-10-01' AND o.created_at < DATE '2026-01-01'
      ),
      total_orders AS (
        SELECT COUNT(DISTINCT order_id) AS total_orders FROM base_orders
      ),
      status_list AS (
        SELECT status_name FROM (VALUES ('pending'),('confirmed'),('paid'),('partially_fulfilled'),('fulfilled'),('cancelled'),('refunded')) AS s(status_name)
      ),
      first_reach AS (
        SELECT bo.order_id, osh.to_status AS status_name, MIN(osh.created_at) AS first_reached_at
        FROM base_orders bo
        JOIN order_status_history osh ON osh.order_id = bo.order_id
        WHERE osh.to_status IN ('pending','confirmed','paid','partially_fulfilled','fulfilled','cancelled','refunded')
        GROUP BY bo.order_id, osh.to_status
      ),
      status_metrics AS (
        SELECT
          sl.status_name,
          COUNT(DISTINCT fr.order_id) AS orders_reaching_status,
          PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY (EXTRACT(EPOCH FROM (fr.first_reached_at - bo.order_created_at)) / 3600.0)) AS median_hours
        FROM status_list sl
        LEFT JOIN first_reach fr ON fr.status_name = sl.status_name
        LEFT JOIN base_orders bo ON bo.order_id = fr.order_id
        GROUP BY sl.status_name
      )
      SELECT
        sm.status_name AS status,
        COALESCE(sm.orders_reaching_status, 0) AS orders_reaching_status,
        ROUND(COALESCE(sm.orders_reaching_status, 0) * 100.0 / t.total_orders, 1) AS pct_of_total_orders,
        ROUND(sm.median_hours, 1) AS median_hours_to_status
      FROM status_metrics sm
      CROSS JOIN total_orders t
      ORDER BY pct_of_total_orders DESC, sm.status_name ASC
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: status, orders_reaching_status, pct_of_total_orders, median_hours_to_status
    • Bea calls submit
      WITH base_orders AS (
        SELECT o.id AS order_id, o.tenant_id, o.created_at AS order_created_at
        FROM orders o
        JOIN (
          SELECT o2.tenant_id, COUNT(*) AS order_count
          FROM orders o2
          WHERE o2.created_at >= DATE '2025-10-01' AND o2.created_at < DATE '2026-01-01'
          GROUP BY o2.tenant_id
          ORDER BY order_count DESC
          LIMIT 10
        ) top_tenants ON top_tenants.tenant_id = o.tenant_id
        WHERE o.created_at >= DATE '2025-10-01' AND o.created_at < DATE '2026-01-01'
      ),
      total_orders AS (
        SELECT COUNT(DISTINCT order_id) AS total_orders FROM base_orders
      ),
      status_list AS (
        SELECT status_name FROM (VALUES ('pending'),('confirmed'),('paid'),('partially_fulfilled'),('fulfilled'),('cancelled'),('refunded')) AS s(status_name)
      ),
      first_reach AS (
        SELECT bo.order_id, osh.to_status AS status_name, MIN(osh.created_at) AS first_reached_at
        FROM base_orders bo
        JOIN order_status_history osh ON osh.order_id = bo.order_id
        WHERE osh.to_status IN ('pending','confirmed','paid','partially_fulfilled','fulfilled','cancelled','refunded')
        GROUP BY bo.order_id, osh.to_status
      ),
      status_metrics AS (
        SELECT
          sl.status_name,
          COUNT(DISTINCT fr.order_id) AS orders_reaching_status,
          PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY (EXTRACT(EPOCH FROM (fr.first_reached_at - bo.order_created_at)) / 3600.0)) AS median_hours
        FROM status_list sl
        LEFT JOIN first_reach fr ON fr.status_name = sl.status_name
        LEFT JOIN base_orders bo ON bo.order_id = fr.order_id
        GROUP BY sl.status_name
      )
      SELECT
        sm.status_name AS status,
        COALESCE(sm.orders_reaching_status, 0) AS orders_reaching_status,
        ROUND(COALESCE(sm.orders_reaching_status, 0) * 100.0 / t.total_orders, 1) AS pct_of_total_orders,
        ROUND(sm.median_hours, 1) AS median_hours_to_status
      FROM status_metrics sm
      CROSS JOIN total_orders t
      ORDER BY pct_of_total_orders DESC, sm.status_name ASC
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • BeaSubmitted. Summary of what I did and the one deviation: - **Base population**: orders created 2025-10-01 to 2025-12-31 (exclusive upper bound 2026-01-01), restricted to tenants. - **Tenant selection caveat**: the request referenced "the same 10 tenants as the Q4 funnel report," but no tenant names were provided to me and I have no access to that companion request. I substituted the 10 tenants with the highest order counts in the Q4 2025 window as a defensible proxy, and flagged this clearly in the submit note so the analyst can correct it if the actual list differs. - **Grain**: one row per status (pending, confirmed, paid, partially_fulfilled, fulfilled, cancelled, refunded). - **Measures**: orders reaching each status (count of distinct orders with at least one `order_status_history` row with that `to_status`), pct of total base-population orders, and median hours from `orders.created_at` to the first time that status was reached (via `PERCENTILE_CONT`). - **Ordering**: pct descending, tie-break by status name, per spec.