DB Olympics

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

Subscription merchants: do customers browse before vs. after their first order?

“Is the near-zero web-session count for subscription merchants a tracking blind spot, or does it reflect real behavior — customers browsing once to sign up and then never browsing again?”

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 SQLViktorOld-school warehouse engineer: explicit JOIN chains straight to the base tables, derived tables, GROUP BY with CASE pivots, few CTEs.

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 2

    Subject: Good start — let's dig into the cart leak

    Emeka, this is exactly the kind of clarity I needed. The cart-stage bleed is the headline, and I agree we should lead with it — but a two-thirds drop-off that lingers for days, converting at under 20%, isn't something we can act on until we know why people stall. "Add cart recovery emails" is a guess until we understand the shape of the hesitation. That's my main ask for this week.

    I also want to flag one thing before we build a roadmap slide on repeat purchase and returns: the return-vs-repeat gap and the delivery/returns pattern are both flagged as not-yet-causal, confounded by vertical. I don't want to present an 8-point "returns hurt loyalty" number to the exec team if it's really just "apparel behaves differently." Let's firm that up before it goes in a deck, but it's not urgent this week.

    Questions for this week:

    1. What does a stalled cart actually contain, and does it differ from a fast-converting one? I'm thinking about basket size, price point, discount/coupon presence — anything that hints at why someone hesitates for hours or days instead of minutes.
    2. Is the cart bleed getting better, worse, or flat over the past year? If it's been stable at ~70% for two years, that's a structural product problem; if it's worsening recently, something changed (a checkout step, a fee, a release) that we should find and reverse.
    3. Of the carts that eventually do convert after sitting a while, is there a nudge already happening (a reminder, a return visit, a discount) that we could learn from and do more deliberately? I want to know if recovery is already happening organically before we invest in prompting it.
    4. On the subscription merchants' tracking gap — is this just a blind spot in how we instrument recurring orders, or is it telling us something about how those shoppers actually behave (e.g., they never browse again after the first signup)? A quick gut-check is enough this week; no need for a deep investigation yet.

    Keep the returns/delivery vertical-control work and the causal retention question on the list for whenever a slot frees up, but the cart story is what I want to walk into the roadmap review with real conviction on.

    Thanks, Emeka — strong week.

    Jonas

  3. Emeka Obi plans the night

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

    Jonas's email has an implicit priority: Q1 (what's in a stalled cart) and Q2 (is the bleed trending worse) are "my main ask for this week"; Q3 (organic recovery) is also wanted but reads as a natural follow-up once we understand the shape; Q4 is explicitly "a quick gut-check... no need for deep investigation." So tonight I'm doing Q1, Q2, and a lightweight version of Q4, saving Q3 for night 2 since it benefits from what we learn tonight (e.g., whether stalled carts later convert with a discount attached is itself a clue about organic recovery).

    Design notes:

    • For Q1, carts.item_count and carts.subtotal are populated regardless of cart status, so I can compare basket size across ALL carts by duration bucket. Discount/coupon presence, however, only exists on the order side (order_discounts/coupon_redemptions) — there's nothing on an abandoned cart to show a discount was being attempted. So I've scoped the discount question to "among carts that did convert, how often did the resulting order carry a discount, by how long the cart took to convert" — flagged clearly as a narrower sub-question, not a true stalled-vs-fast comparison on discounts.
    • For Q2, I'm using the same 5 duration buckets from last week's cart-timing reports, by month across all of 2025, to see if the mix is shifting (e.g., more carts falling into the 1-7 day "bleed" bucket recently) or if the overall cart→order conversion rate itself is moving. I'll also watch for anything around November 2025, since Platform Engineering flagged a cart-reminder email bug that week — relevant context, not something I'm acting on yet.
    • For Q4 ("gut check"), I'm testing the blind-spot-vs-behavior question directly: for the 4 subscription merchants, do customers have tracked sessions before their first order (proving tracking works) but little or none after (suggesting they genuinely stop browsing once subscribed) — vs. near-zero sessions even before the first order (which would point to a tracking gap instead).

    All three use the 6 retail tenants (Q1, Q2) or 4 subscription tenants (Q4) established in last week's work, so the engineer has a concrete, named population to filter on rather than re-deriving "top merchants."

    Subscription merchants: do customers browse before vs. after their first order?
    Question
    Is the near-zero web-session count for subscription merchants a tracking blind spot, or does it reflect real behavior — customers browsing once to sign up and then never browsing again?
    One row per
    subscription-merchant tenant
    Columns
    • tenant name
    • customers (distinct users with at least one order)
    • pct of customers with any session before their first order
    • pct of customers with any session after their first order
    • avg sessions after first order, per customer
  4. Viktor 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)
    SELECT t.name AS tenant_name,
      COUNT(*) AS customers,
      ROUND(100.0 * SUM(CASE WHEN cust.before_cnt > 0 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_with_session_before,
      ROUND(100.0 * SUM(CASE WHEN cust.after_cnt > 0 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_with_session_after,
      ROUND(AVG(cust.after_cnt), 2) AS avg_sessions_after_first_order_per_customer
    FROM (
      SELECT fo.tenant_id, fo.user_id, fo.first_order_at,
        (SELECT COUNT(*) FROM web_sessions ws WHERE ws.tenant_id = fo.tenant_id AND ws.user_id = fo.user_id AND ws.started_at <= fo.first_order_at) AS before_cnt,
        (SELECT COUNT(*) FROM web_sessions ws WHERE ws.tenant_id = fo.tenant_id AND ws.user_id = fo.user_id AND ws.started_at > fo.first_order_at) AS after_cnt
      FROM (
        SELECT tenant_id, user_id, MIN(placed_at) AS first_order_at
        FROM orders
        WHERE tenant_id IN (SELECT id FROM tenants WHERE name IN ('Brennquell Kaffee','Spice Route Box','Grind Collective','Honeycomb Larder'))
        GROUP BY tenant_id, user_id
      ) fo
    ) cust
    JOIN tenants t ON t.id = cust.tenant_id
    GROUP BY t.name
    ORDER BY t.name

    “Compiles cleanly and matches the requested grain and columns.”

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

    “Submitted. One note for the analyst: since web_sessions only link to a customer once user_id is set, any anonymous pre-signup browsing (common for a "browse once to sign up" pattern) won't show up in pct_with_session_before — that's a real limitation of the session-to-user linkage, not a query choice, and worth flagging if the before-percentage comes back low.”

  5. The report comes back overnight

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

    tenant_name       | customers | pct_with_session_before | pct_with_session_after | avg_sessions_after_first_order_per_custo
    ------------------+-----------+-------------------------+------------------------+-----------------------------------------
    Brennquell Kaffee |       659 |                    20.2 |                   50.7 |                                     3.13
    Grind Collective  |       145 |                    15.9 |                   49.0 |                                     2.86
    Honeycomb Larder  |       115 |                    30.4 |                   69.6 |                                     4.13
    Spice Route Box   |       158 |                    22.8 |                   42.4 |                                     2.44
    (4 rows)
  6. What came of it

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

    It builds on:

  7. It becomes benchmark query q289

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

    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision; AVG over an integer computed as a decimal: AVG(CAST(x AS NUMERIC(18,6)))
    • made explicita selected t.col gets AS col, its standard name made explicit (3×)
    • determinismthe standard leaves the place of NULLs in a sort to the implementation: NULLS LAST ascending, NULLS FIRST descending, as in the reference results
    The benchmark text, against the submitted SQL
    SELECT t.name AS tenant_name,  COUNT(*) AS customers,  ROUND(100.0 * SUM(CASE WHEN cust.before_cnt > 0 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_with_session_before,  ROUND(100.0 * SUM(CASE WHEN cust.after_cnt > 0 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_with_session_after,  ROUND(AVG(cust.after_cnt), 2) AS avg_sessions_after_first_order_per_customer  100.0 * SUM(CASE WHEN cust.before_cnt > 0 THEN 1 ELSE 0 END) / COUNT(*) AS pct_with_session_before,  100.0 * SUM(CASE WHEN cust.after_cnt > 0 THEN 1 ELSE 0 END) / COUNT(*) AS pct_with_session_after,  AVG(CAST(cust.after_cnt AS NUMERIC(18,6))) AS avg_sessions_after_first_order_per_customerFROM (  SELECT fo.tenant_id, fo.user_id, fo.first_order_at,  SELECT fo.tenant_id AS tenant_id, fo.user_id AS user_id, fo.first_order_at AS first_order_at,    (SELECT COUNT(*) FROM web_sessions ws WHERE ws.tenant_id = fo.tenant_id AND ws.user_id = fo.user_id AND ws.started_at <= fo.first_order_at) AS before_cnt,    (SELECT COUNT(*) FROM web_sessions ws WHERE ws.tenant_id = fo.tenant_id AND ws.user_id = fo.user_id AND ws.started_at > fo.first_order_at) AS after_cnt  FROM (    SELECT tenant_id, user_id, MIN(placed_at) AS first_order_at    FROM orders    WHERE tenant_id IN (SELECT id FROM tenants WHERE name IN ('Brennquell Kaffee','Spice Route Box','Grind Collective','Honeycomb Larder'))    GROUP BY tenant_id, user_id  ) fo) custJOIN tenants t ON t.id = cust.tenant_idGROUP BY t.nameORDER BY t.nameORDER BY t.name NULLS LAST
    SELECT t.name AS tenant_name,
      COUNT(*) AS customers,
      100.0 * SUM(CASE WHEN cust.before_cnt > 0 THEN 1 ELSE 0 END) / COUNT(*) AS pct_with_session_before,
      100.0 * SUM(CASE WHEN cust.after_cnt > 0 THEN 1 ELSE 0 END) / COUNT(*) AS pct_with_session_after,
      AVG(CAST(cust.after_cnt AS NUMERIC(18,6))) AS avg_sessions_after_first_order_per_customer
    FROM (
      SELECT fo.tenant_id AS tenant_id, fo.user_id AS user_id, fo.first_order_at AS first_order_at,
        (SELECT COUNT(*) FROM web_sessions ws WHERE ws.tenant_id = fo.tenant_id AND ws.user_id = fo.user_id AND ws.started_at <= fo.first_order_at) AS before_cnt,
        (SELECT COUNT(*) FROM web_sessions ws WHERE ws.tenant_id = fo.tenant_id AND ws.user_id = fo.user_id AND ws.started_at > fo.first_order_at) AS after_cnt
      FROM (
        SELECT tenant_id, user_id, MIN(placed_at) AS first_order_at
        FROM orders
        WHERE tenant_id IN (SELECT id FROM tenants WHERE name IN ('Brennquell Kaffee','Spice Route Box','Grind Collective','Honeycomb Larder'))
        GROUP BY tenant_id, user_id
      ) fo
    ) cust
    JOIN tenants t ON t.id = cust.tenant_id
    GROUP BY t.name
    ORDER BY t.name NULLS LAST
    100 ms1 sHyper180 msDuckDB141 msGizmoSQL137 msclickhouse_brew827 msFirebolt496 msDataFusionerrorClickHousewrong result
    • DataFusionThis feature is not implemented: Physical plan does not support logical expression ScalarSubquery(<subquery>)
    • ClickHouserow 1: col 5: "2.9223230018432655845263132252275661586183" vs "6.298815100900197"
  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

    Viktor's whole session, 7 turns
    • The request, as Viktor received it
      Report request from Emeka Obi (analyst for the VP Product):
      
      Title: Subscription merchants: do customers browse before vs. after their first order?
      Question: Is the near-zero web-session count for subscription merchants a tracking blind spot, or does it reflect real behavior — customers browsing once to sign up and then never browsing again?
      Grain: one row per subscription-merchant tenant
      Columns:
      - tenant name
      - customers (distinct users with at least one order)
      - pct of customers with any session before their first order
      - pct of customers with any session after their first order
      - avg sessions after first order, per customer
      Details: Tenants: the 4 Food & Coffee subscription merchants (Brennquell Kaffee, Spice Route Box, Grind Collective, Honeycomb Larder). For each tenant, customers = distinct users (tenant_id, user_id) with at least one order (any status) in orders. For each customer, first_order_at = min(orders.placed_at). pct_with_session_before = % of customers with at least one web_sessions row where started_at <= first_order_at. pct_with_session_after = % of customers with at least one web_sessions row where started_at > first_order_at (any time after, through 2025-12-31). avg_sessions_after_first_order_per_customer = average count of web_sessions rows with started_at > first_order_at, across all customers (including those with zero). Round percentages to 1 decimal and the average to 2 decimals. Order rows by tenant name.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Viktor calls dry_run
      SELECT t.name AS tenant_name,
        COUNT(*) AS customers,
        ROUND(100.0 * SUM(CASE WHEN cust.before_cnt > 0 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_with_session_before,
        ROUND(100.0 * SUM(CASE WHEN cust.after_cnt > 0 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_with_session_after,
        ROUND(AVG(cust.after_cnt), 2) AS avg_sessions_after_first_order_per_customer
      FROM (
        SELECT fo.tenant_id, fo.user_id, fo.first_order_at,
          (SELECT COUNT(*) FROM web_sessions ws WHERE ws.tenant_id = fo.tenant_id AND ws.user_id = fo.user_id AND ws.started_at <= fo.first_order_at) AS before_cnt,
          (SELECT COUNT(*) FROM web_sessions ws WHERE ws.tenant_id = fo.tenant_id AND ws.user_id = fo.user_id AND ws.started_at > fo.first_order_at) AS after_cnt
        FROM (
          SELECT tenant_id, user_id, MIN(placed_at) AS first_order_at
          FROM orders
          WHERE tenant_id IN (SELECT id FROM tenants WHERE name IN ('Brennquell Kaffee','Spice Route Box','Grind Collective','Honeycomb Larder'))
          GROUP BY tenant_id, user_id
        ) fo
      ) cust
      JOIN tenants t ON t.id = cust.tenant_id
      GROUP BY t.name
      ORDER BY t.name
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: tenant_name, customers, pct_with_session_before, pct_with_session_after, avg_sessions_after_first_order_per_customer
    • ViktorCompiles cleanly and matches the requested grain and columns.
    • Viktor calls submit
      SELECT t.name AS tenant_name,
        COUNT(*) AS customers,
        ROUND(100.0 * SUM(CASE WHEN cust.before_cnt > 0 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_with_session_before,
        ROUND(100.0 * SUM(CASE WHEN cust.after_cnt > 0 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_with_session_after,
        ROUND(AVG(cust.after_cnt), 2) AS avg_sessions_after_first_order_per_customer
      FROM (
        SELECT fo.tenant_id, fo.user_id, fo.first_order_at,
          (SELECT COUNT(*) FROM web_sessions ws WHERE ws.tenant_id = fo.tenant_id AND ws.user_id = fo.user_id AND ws.started_at <= fo.first_order_at) AS before_cnt,
          (SELECT COUNT(*) FROM web_sessions ws WHERE ws.tenant_id = fo.tenant_id AND ws.user_id = fo.user_id AND ws.started_at > fo.first_order_at) AS after_cnt
        FROM (
          SELECT tenant_id, user_id, MIN(placed_at) AS first_order_at
          FROM orders
          WHERE tenant_id IN (SELECT id FROM tenants WHERE name IN ('Brennquell Kaffee','Spice Route Box','Grind Collective','Honeycomb Larder'))
          GROUP BY tenant_id, user_id
        ) fo
      ) cust
      JOIN tenants t ON t.id = cust.tenant_id
      GROUP BY t.name
      ORDER BY t.name
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • ViktorSubmitted. One note for the analyst: since `web_sessions` only link to a customer once `user_id` is set, any anonymous pre-signup browsing (common for a "browse once to sign up" pattern) won't show up in `pct_with_session_before` — that's a real limitation of the session-to-user linkage, not a query choice, and worth flagging if the before-percentage comes back low.