DB Olympics

Queries · q290 · VP Product · week 2, night 2

Cart conversion by week at Cragsley Outfitters around the Nov 24 reminder-email outage

“Did cart conversion dip during the week cart-reminder emails broke (week of Nov 24, 2025), suggesting those reminders meaningfully nudge stalled carts toward purchase?”

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 SQLInesThinks in window functions: ranks, running totals, LAG/LEAD for comparisons, percent-of-total.

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

    Reading last night's reports:

    Report 1 (cart contents) is the headline for Q1: fast-converting carts (<1hr) average 2.79 items and €242 (median €131), while stalled carts (1-24h, 1-7d) average only 1.6-1.8 items and €169-210 (median €75-84). So hesitation concentrates in smaller, lower-value, mostly single-item carts — not "sticker shock" on big purchases. Interestingly, per-item price is actually higher in stalled carts (~€97-117/item) than fast carts (~€87/item), suggesting the stalling archetype may be "one pricier item, deliberated over" rather than "big basket, abandoned." I want one more cut on price tier specifically to confirm this before I put it in front of Jonas. Caveat: the discount-presence column is only observable among carts that converted (abandoned carts never reach an order, so there's no discount record) — it's a real but narrower sub-question than "stalled vs fast," and I've noted that.

    Report 2 (monthly trend) shows a genuinely flat cart→order conversion rate (34-38%) Jan-Oct — good evidence the bleed is structural, not worsening, which is itself useful for Jonas. But Nov (37.7%) and especially Dec's lower 1-7-day resolution rate (62.5% vs typical 69-73%) are suspect: cart volume nearly doubled in Nov/Dec (holiday season) and any cart created in the back half of December simply hasn't had a full 7 days to resolve against our Dec-31 data cutoff — that's right-censoring, not a real behavior change, and I should not over-read it. More interesting: last week's company channel flagged that cart-reminder emails broke for exactly the week of Nov 24 at 10 merchants including Cragsley Outfitters (one of our 6 retail tenants). That's a natural experiment sitting right there, and it answers two of Jonas's questions at once — Q2 ("what changed") and Q3 ("is a nudge already happening") — so I'm prioritizing it tonight.

    Report 3 (subscription tracking gap) flipped my expectation: "after first order" session coverage is actually substantial (42-70% of customers have a session after, averaging 2.4-4.1 of them) — these shoppers do keep coming back and browsing. The real gap is "before first order" (only 16-30% have a tracked session pre-signup), which looks more like a tracking gap at the signup moment than a behavioral finding. That's a sufficient "quick gut-check" per Jonas's ask — I'm not spending more slots on this this week.

    Tonight's 3 requests: (1) the Nov 24 cart-reminder-outage natural experiment at Cragsley Outfitters, (2) a price-tier cut to sharpen the Q1 "why" story, (3) a direct empirical test of whether an email nudge correlates with recovery among stalled carts generally (not just the outage week) — the clearest way to answer Jonas's "is a nudge already happening that we could learn from."

    Cart conversion by week at Cragsley Outfitters around the Nov 24 reminder-email outage
    Question
    Did cart conversion dip during the week cart-reminder emails broke (week of Nov 24, 2025), suggesting those reminders meaningfully nudge stalled carts toward purchase?
    One row per
    ISO week of cart creation, for Cragsley Outfitters only
    Columns
    • week start date
    • carts created
    • overall conversion pct (cart eventually became an order)
    • pct converted within 1 hour
    • pct converted in 1-7 days
  4. Ines 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 tenant as (
      select id as tenant_id
      from tenants
      where name = 'Cragsley Outfitters'
    ),
    cart_weeks as (
      select
        c.id,
        c.status,
        c.created_at,
        c.converted_at,
        cd.week_start
      from carts c
      join tenant t on c.tenant_id = t.tenant_id
      join calendar_days cd on cd.calendar_date = c.created_at::date
      where cd.week_start >= date '2025-10-06'
        and cd.week_start <= date '2025-12-22'
    )
    select
      week_start,
      count(*) as carts_created,
      round(100.0 * sum(case when status = 'converted' then 1 else 0 end) / count(*), 1) as overall_conversion_pct,
      round(100.0 * sum(case when status = 'converted' and converted_at is not null
                and extract(epoch from (converted_at - created_at)) / 3600.0 <= 1
                then 1 else 0 end) / count(*), 1) as pct_converted_within_1_hour,
      round(100.0 * sum(case when status = 'converted' and converted_at is not null
                and extract(epoch from (converted_at - created_at)) / 3600.0 >= 24
                and extract(epoch from (converted_at - created_at)) / 3600.0 <= 168
                then 1 else 0 end) / count(*), 1) as pct_converted_1_to_7_days
    from cart_weeks
    group by week_start
    order by week_start asc

    “Query compiles cleanly with the expected columns.”

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

    “Submitted. The report gives one row per ISO week (Oct 6 – Dec 22, 2025, 12 weeks) for Cragsley Outfitters with carts created, overall conversion %, and the two conversion-speed buckets, so the Nov 24 week sits in the middle of the table for easy before/after comparison.”

  5. The report comes back overnight

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

    week_start | carts_created | overall_conversion_pct | pct_converted_within_1_hour | pct_converted_1_to_7_days
    -----------+---------------+------------------------+-----------------------------+--------------------------
    2025-10-06 |           213 |                   31.5 |                        24.9 |                       5.6
    2025-10-13 |           236 |                   32.2 |                        28.0 |                       3.0
    2025-10-20 |           231 |                   29.0 |                        23.4 |                       4.3
    2025-10-27 |           207 |                   38.6 |                        32.9 |                       5.3
    2025-11-03 |           250 |                   36.0 |                        27.2 |                       7.2
    2025-11-10 |           241 |                   31.1 |                        24.1 |                       5.0
    2025-11-17 |           304 |                   41.1 |                        33.9 |                       5.9
    2025-11-24 |           524 |                   43.5 |                        34.9 |                       7.4
    2025-12-01 |           478 |                   38.5 |                        31.4 |                       6.1
    2025-12-08 |           345 |                   38.3 |                        29.0 |                       8.7
    2025-12-15 |           333 |                   37.5 |                        27.9 |                       7.8
    2025-12-22 |           253 |                   40.3 |                        33.2 |                       6.3
    (12 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 q290

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

    • spellingcast
    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision
    • numericsdocs/NUMERICS.md: EXTRACT(EPOCH FROM (b - a)) cast to NUMERIC(18,6) (3×)
    • made explicita selected t.col gets AS col, its standard name made explicit (5×)
    • placeholderseconds between (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
    with tenant as (  select id as tenant_id  from tenants  where name = 'Cragsley Outfitters'),cart_weeks as (  select    c.id,    c.status,    c.created_at,    c.converted_at,    cd.week_start    c.id AS id,    c.status AS status,    c.created_at AS created_at,    c.converted_at AS converted_at,    cd.week_start AS week_start  from carts c  join tenant t on c.tenant_id = t.tenant_id  join calendar_days cd on cd.calendar_date = c.created_at::date  join calendar_days cd on cd.calendar_date = CAST(c.created_at AS date)  where cd.week_start >= date '2025-10-06'    and cd.week_start <= date '2025-12-22')select  week_start,  count(*) as carts_created,  round(100.0 * sum(case when status = 'converted' then 1 else 0 end) / count(*), 1) as overall_conversion_pct,  round(100.0 * sum(case when status = 'converted' and converted_at is not null            and extract(epoch from (converted_at - created_at)) / 3600.0 <= 1            then 1 else 0 end) / count(*), 1) as pct_converted_within_1_hour,  round(100.0 * sum(case when status = 'converted' and converted_at is not null            and extract(epoch from (converted_at - created_at)) / 3600.0 >= 24            and extract(epoch from (converted_at - created_at)) / 3600.0 <= 168            then 1 else 0 end) / count(*), 1) as pct_converted_1_to_7_days  100.0 * sum(case when status = 'converted' then 1 else 0 end) / count(*) as overall_conversion_pct,  100.0 * sum(case when status = 'converted' and converted_at is not null      and {{seconds_between(created_at, converted_at)}} / 3600.0 <= 1      then 1 else 0 end) / count(*) as pct_converted_within_1_hour,  100.0 * sum(case when status = 'converted' and converted_at is not null      and {{seconds_between(created_at, converted_at)}} / 3600.0 >= 24      and {{seconds_between(created_at, converted_at)}} / 3600.0 <= 168      then 1 else 0 end) / count(*) as pct_converted_1_to_7_daysfrom cart_weeksgroup by week_startorder by week_start ascorder by week_start asc NULLS LAST
    with tenant as (
      select id as tenant_id
      from tenants
      where name = 'Cragsley Outfitters'
    ),
    cart_weeks as (
      select
        c.id AS id,
        c.status AS status,
        c.created_at AS created_at,
        c.converted_at AS converted_at,
        cd.week_start AS week_start
      from carts c
      join tenant t on c.tenant_id = t.tenant_id
      join calendar_days cd on cd.calendar_date = CAST(c.created_at AS date)
      where cd.week_start >= date '2025-10-06'
        and cd.week_start <= date '2025-12-22'
    )
    select
      week_start,
      count(*) as carts_created,
      100.0 * sum(case when status = 'converted' then 1 else 0 end) / count(*) as overall_conversion_pct,
      100.0 * sum(case when status = 'converted' and converted_at is not null
          and CAST(EXTRACT(EPOCH FROM (converted_at - created_at)) AS NUMERIC(18,6)) / 3600.0 <= 1
          then 1 else 0 end) / count(*) as pct_converted_within_1_hour,
      100.0 * sum(case when status = 'converted' and converted_at is not null
          and CAST(EXTRACT(EPOCH FROM (converted_at - created_at)) AS NUMERIC(18,6)) / 3600.0 >= 24
          and CAST(EXTRACT(EPOCH FROM (converted_at - created_at)) AS NUMERIC(18,6)) / 3600.0 <= 168
          then 1 else 0 end) / count(*) as pct_converted_1_to_7_days
    from cart_weeks
    group by week_start
    order by week_start asc NULLS LAST
    10 ms100 ms1 sHyper78.2 msDuckDB50.9 msGizmoSQL88.9 msclickhouse_brew201 msFirebolt126 msDataFusion85.5 msClickHouse202 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

    Ines's whole session, 7 turns
    • The request, as Ines received it
      Report request from Emeka Obi (analyst for the VP Product):
      
      Title: Cart conversion by week at Cragsley Outfitters around the Nov 24 reminder-email outage
      Question: Did cart conversion dip during the week cart-reminder emails broke (week of Nov 24, 2025), suggesting those reminders meaningfully nudge stalled carts toward purchase?
      Grain: one row per ISO week of cart creation, for Cragsley Outfitters only
      Columns:
      - week start date
      - carts created
      - overall conversion pct (cart eventually became an order)
      - pct converted within 1 hour
      - pct converted in 1-7 days
      Details: Tenant: Cragsley Outfitters only (look up tenant_id by name). Population: carts with created_at falling in ISO weeks starting 2025-10-06 through 2025-12-22 inclusive (12 weeks) — stop at week-start 2025-12-22 so every included cart has a full 7-day window to resolve before the 2025-12-31 data cutoff (avoids right-censoring bias). overall_conversion_pct = % of that week's carts with status='converted' (no time limit on when). pct_converted_within_1_hour and pct_converted_in_1_7_days use resolution_time = converted_at - created_at (hours/days), same definitions as prior cart-timing reports. Order rows by week start ascending. The week of 2025-11-24 should be visually easy to compare against the surrounding weeks.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Ines calls dry_run
      with tenant as (
        select id as tenant_id
        from tenants
        where name = 'Cragsley Outfitters'
      ),
      cart_weeks as (
        select
          c.id,
          c.status,
          c.created_at,
          c.converted_at,
          cd.week_start
        from carts c
        join tenant t on c.tenant_id = t.tenant_id
        join calendar_days cd on cd.calendar_date = c.created_at::date
        where cd.week_start >= date '2025-10-06'
          and cd.week_start <= date '2025-12-22'
      )
      select
        week_start,
        count(*) as carts_created,
        round(100.0 * sum(case when status = 'converted' then 1 else 0 end) / count(*), 1) as overall_conversion_pct,
        round(100.0 * sum(case when status = 'converted' and converted_at is not null
                  and extract(epoch from (converted_at - created_at)) / 3600.0 <= 1
                  then 1 else 0 end) / count(*), 1) as pct_converted_within_1_hour,
        round(100.0 * sum(case when status = 'converted' and converted_at is not null
                  and extract(epoch from (converted_at - created_at)) / 3600.0 >= 24
                  and extract(epoch from (converted_at - created_at)) / 3600.0 <= 168
                  then 1 else 0 end) / count(*), 1) as pct_converted_1_to_7_days
      from cart_weeks
      group by week_start
      order by week_start asc
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: week_start, carts_created, overall_conversion_pct, pct_converted_within_1_hour, pct_converted_1_to_7_days
    • InesQuery compiles cleanly with the expected columns.
    • Ines calls submit
      with tenant as (
        select id as tenant_id
        from tenants
        where name = 'Cragsley Outfitters'
      ),
      cart_weeks as (
        select
          c.id,
          c.status,
          c.created_at,
          c.converted_at,
          cd.week_start
        from carts c
        join tenant t on c.tenant_id = t.tenant_id
        join calendar_days cd on cd.calendar_date = c.created_at::date
        where cd.week_start >= date '2025-10-06'
          and cd.week_start <= date '2025-12-22'
      )
      select
        week_start,
        count(*) as carts_created,
        round(100.0 * sum(case when status = 'converted' then 1 else 0 end) / count(*), 1) as overall_conversion_pct,
        round(100.0 * sum(case when status = 'converted' and converted_at is not null
                  and extract(epoch from (converted_at - created_at)) / 3600.0 <= 1
                  then 1 else 0 end) / count(*), 1) as pct_converted_within_1_hour,
        round(100.0 * sum(case when status = 'converted' and converted_at is not null
                  and extract(epoch from (converted_at - created_at)) / 3600.0 >= 24
                  and extract(epoch from (converted_at - created_at)) / 3600.0 <= 168
                  then 1 else 0 end) / count(*), 1) as pct_converted_1_to_7_days
      from cart_weeks
      group by week_start
      order by week_start asc
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • InesSubmitted. The report gives one row per ISO week (Oct 6 – Dec 22, 2025, 12 weeks) for Cragsley Outfitters with carts created, overall conversion %, and the two conversion-speed buckets, so the Nov 24 week sits in the middle of the table for easy before/after comparison.