DB Olympics

Queries · q047 · VP Customer Experience · week 1, night 4

CSAT score by ticket resolution-time bucket

“Does a slower ticket resolution actually translate into a lower satisfaction score?”

Manager · asked the questionDaniel AchterbergVP Customer Experience. Former head of support at a marketplace. Merchants run their helpdesk on Tidewick, so their shoppers' experience is partly Tidewick's reputation.
Analyst · planned the reportMateo RuizComparative: likes year-over-year and peer comparisons across merchants and countries.
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. Daniel Achterberg writes to Mateo Ruiz

    The manager, at the start of week 1

    Subject: Where I'd like to start — support & shopper experience

    Hi Mateo,

    Good to have you on this with me. I'm still building my map of the business, so I want to keep this week broad — I'd rather get a solid first read on a few big questions than a deep dive on one. Here's what's on my mind, roughly in priority order:

    1. How are we doing on response and resolution times, and is it getting better or worse? I keep hearing "we're slow" anecdotally but have no sense of the trend, or whether it varies a lot by merchant. I need a baseline I can put in front of the leadership team.
    2. Where is the support load actually coming from? What kinds of problems are shoppers contacting merchants about most, and has that mix been shifting lately? I want to know if there's a theme we could fix at the platform level instead of merchant-by-merchant.
    3. What does the returns and refunds experience look like from the shopper's side? How often shoppers end up wanting their money back, how long that takes to resolve, and whether that's a smooth process or a source of frustration. This feels close to our reputation.
    4. How satisfied are shoppers overall, and where is satisfaction weakest? I'd like a sense of satisfaction levels and whether certain merchants or situations stand out as problem spots.
    5. (If there's room this week) What are reviews and ratings telling us? Even a first look at how shoppers rate their purchases, and whether that lines up with what we see in support, would help me understand if there are blind spots in the ticket data.

    What I need these for: I'm meeting merchant success and the exec team soon and want a credible, numbers-backed view of shopper experience — not just war stories. I also want to start spotting the two or three things that, if we fixed them, would move the needle most broadly across merchants, rather than chasing individual complaints.

    Given the slot constraints, let's prioritize #1 through #3 this week if we can't fit all five — those feel most urgent to me. Happy to talk through any of this before you lock in requests.

    Thanks, Daniel

  2. Mateo Ruiz plans the night

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

    Night 3 confirmed the Studio Vrij story with real operational data: their placed-to-delivered time spiked to 9.3 days avg and 70.1% of shipments took over 7 days in May 2025 alone (vs 0-20% other months, platform average stayed 3-5%) - a genuine single-merchant fulfillment breakdown that explains the ticket spike, not a support-side or platform-wide issue. Useful side-finding: platform-wide % of shipments over 7 days creeps up in Nov/Dec 2025 (7-12% vs ~3-5% normally) alongside a real volume surge (1600→2600+ shipments/month) - that's consistent with capacity-constrained holiday fulfillment driving the recurring Nov/Dec ticket spikes seen across all three years, which is the platform-level theme Daniel asked about. The returns-by-merchant report is reassuring: resolve time (~7.5-8.5 days) and rejection rates are consistent across merchants regardless of return-rate level (which mostly tracks vertical - apparel/footwear naturally higher, food/beauty lower) - a clean contrast to how much support-speed varies by merchant. CSAT is noisy but its worst months (Dec-23, Dec-24/Jan-25, Jun-25, Dec-25) line up exactly with the ticket volume/slowness spikes already identified, suggesting speed drives satisfaction. This is the last night, so I'm using the 3 slots to: (1) get CSAT by merchant so Daniel can see which merchants are the weakest satisfaction spots (mirroring the support-speed and returns peer reports already in hand), (2) directly quantify the CSAT-vs-resolution-speed relationship platform-wide to turn "slow support hurts satisfaction" from a visual coincidence into a measured one - this is the single clearest "fix this and it moves the needle broadly" argument for the memo, and (3) a first look at product review ratings (priority #5) since there's room, to see if it echoes the CSAT dips or reveals a blind spot in ticket data.

    CSAT score by ticket resolution-time bucket
    Question
    Does a slower ticket resolution actually translate into a lower satisfaction score?
    One row per
    resolution-time bucket, platform-wide, across all tickets 2023-2025 that have both a resolution and a CSAT response
    Columns
    • resolution time bucket
    • tickets (count)
    • avg CSAT score (1-5)
    • % satisfied (score >= 4)
    • % dissatisfied (score <= 2)
  3. 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 resolved_tickets_with_csat as (
        select
            t.id as ticket_id,
            c.score as csat_score,
            extract(epoch from (t.resolved_at - t.created_at)) / 3600.0 as resolution_hours
        from tickets t
        join csat_responses c
            on c.ticket_id = t.id
        where t.resolved_at is not null
            and t.created_at >= timestamp '2023-01-01'
            and t.created_at < timestamp '2026-01-01'
    ),
    bucketed_tickets as (
        select
            ticket_id,
            csat_score,
            case
                when resolution_hours < 4 then '<4h'
                when resolution_hours < 24 then '4-24h'
                when resolution_hours < 72 then '1-3 days'
                when resolution_hours < 168 then '3-7 days'
                else '>7 days'
            end as resolution_bucket,
            case
                when resolution_hours < 4 then 0
                when resolution_hours < 24 then 4
                when resolution_hours < 72 then 24
                when resolution_hours < 168 then 72
                else 168
            end as bucket_sort_key
        from resolved_tickets_with_csat
    ),
    bucket_summary as (
        select
            resolution_bucket,
            bucket_sort_key,
            count(*) as ticket_count,
            round(avg(csat_score)::numeric, 2) as avg_csat_score,
            round(100.0 * sum(case when csat_score >= 4 then 1 else 0 end) / count(*), 1) as pct_satisfied,
            round(100.0 * sum(case when csat_score <= 2 then 1 else 0 end) / count(*), 1) as pct_dissatisfied
        from bucketed_tickets
        group by resolution_bucket, bucket_sort_key
    )
    select
        resolution_bucket as resolution_time_bucket,
        ticket_count as tickets,
        avg_csat_score as avg_csat_score,
        pct_satisfied as pct_satisfied,
        pct_dissatisfied as pct_dissatisfied
    from bucket_summary
    order by bucket_sort_key

    “Query compiles cleanly and returns the expected 5 columns at the resolution-bucket grain.”

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

    “Submitted. The report buckets all 2023–2025 tickets with both a resolution and a CSAT response into the five requested resolution-time ranges, with ticket counts, average CSAT, and % satisfied/dissatisfied per bucket, ordered by increasing resolution time.”

  4. The report comes back overnight

    As Mateo Ruiz saw it, from the small dataset the agents worked on

    resolution_time_bucket | tickets | avg_csat_score | pct_satisfied | pct_dissatisfied
    -----------------------+---------+----------------+---------------+-----------------
    <4h                    |     273 |           4.38 |          86.1 |              0.7
    4-24h                  |     712 |           3.82 |          64.3 |              7.9
    1-3 days               |     461 |           2.72 |          21.7 |             41.4
    3-7 days               |     124 |           1.70 |           2.4 |             83.9
    >7 days                |      64 |           1.72 |           1.6 |             82.8
    (5 rows)
  5. What came of it

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

    It builds on:

  6. It becomes benchmark query q047

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

    • spellingcast
    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision; bare NUMERIC cast that only served the rounding removed; AVG over an integer computed as a decimal: AVG(CAST(x AS NUMERIC(18,6)))
    • numericsdocs/NUMERICS.md: EXTRACT(EPOCH FROM (b - a)) cast to NUMERIC(18,6)
    • 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
    The benchmark text, against the submitted SQL
    with resolved_tickets_with_csat as (    select        t.id as ticket_id,        c.score as csat_score,        extract(epoch from (t.resolved_at - t.created_at)) / 3600.0 as resolution_hours        {{seconds_between(t.created_at, t.resolved_at)}} / 3600.0 as resolution_hours    from tickets t    join csat_responses c        on c.ticket_id = t.id    where t.resolved_at is not null        and t.created_at >= timestamp '2023-01-01'        and t.created_at < timestamp '2026-01-01'),bucketed_tickets as (    select        ticket_id,        csat_score,        case            when resolution_hours < 4 then '<4h'            when resolution_hours < 24 then '4-24h'            when resolution_hours < 72 then '1-3 days'            when resolution_hours < 168 then '3-7 days'            else '>7 days'        end as resolution_bucket,        case            when resolution_hours < 4 then 0            when resolution_hours < 24 then 4            when resolution_hours < 72 then 24            when resolution_hours < 168 then 72            else 168        end as bucket_sort_key    from resolved_tickets_with_csat),bucket_summary as (    select        resolution_bucket,        bucket_sort_key,        count(*) as ticket_count,        round(avg(csat_score)::numeric, 2) as avg_csat_score,        round(100.0 * sum(case when csat_score >= 4 then 1 else 0 end) / count(*), 1) as pct_satisfied,        round(100.0 * sum(case when csat_score <= 2 then 1 else 0 end) / count(*), 1) as pct_dissatisfied        avg(cast(csat_score as numeric(18,6))) as avg_csat_score,        100.0 * sum(case when csat_score >= 4 then 1 else 0 end) / count(*) as pct_satisfied,        100.0 * sum(case when csat_score <= 2 then 1 else 0 end) / count(*) as pct_dissatisfied    from bucketed_tickets    group by resolution_bucket, bucket_sort_key)select    resolution_bucket as resolution_time_bucket,    ticket_count as tickets,    avg_csat_score as avg_csat_score,    pct_satisfied as pct_satisfied,    pct_dissatisfied as pct_dissatisfiedfrom bucket_summaryorder by bucket_sort_keyorder by bucket_sort_key NULLS LAST
    with resolved_tickets_with_csat as (
        select
            t.id as ticket_id,
            c.score as csat_score,
            CAST(EXTRACT(EPOCH FROM (t.resolved_at - t.created_at)) AS NUMERIC(18,6)) / 3600.0 as resolution_hours
        from tickets t
        join csat_responses c
            on c.ticket_id = t.id
        where t.resolved_at is not null
            and t.created_at >= timestamp '2023-01-01'
            and t.created_at < timestamp '2026-01-01'
    ),
    bucketed_tickets as (
        select
            ticket_id,
            csat_score,
            case
                when resolution_hours < 4 then '<4h'
                when resolution_hours < 24 then '4-24h'
                when resolution_hours < 72 then '1-3 days'
                when resolution_hours < 168 then '3-7 days'
                else '>7 days'
            end as resolution_bucket,
            case
                when resolution_hours < 4 then 0
                when resolution_hours < 24 then 4
                when resolution_hours < 72 then 24
                when resolution_hours < 168 then 72
                else 168
            end as bucket_sort_key
        from resolved_tickets_with_csat
    ),
    bucket_summary as (
        select
            resolution_bucket,
            bucket_sort_key,
            count(*) as ticket_count,
            avg(cast(csat_score as numeric(18,6))) as avg_csat_score,
            100.0 * sum(case when csat_score >= 4 then 1 else 0 end) / count(*) as pct_satisfied,
            100.0 * sum(case when csat_score <= 2 then 1 else 0 end) / count(*) as pct_dissatisfied
        from bucketed_tickets
        group by resolution_bucket, bucket_sort_key
    )
    select
        resolution_bucket as resolution_time_bucket,
        ticket_count as tickets,
        avg_csat_score as avg_csat_score,
        pct_satisfied as pct_satisfied,
        pct_dissatisfied as pct_dissatisfied
    from bucket_summary
    order by bucket_sort_key NULLS LAST
    10 ms100 msHyper17.7 msDuckDB41.8 msGizmoSQL45.4 msclickhouse_brew59.9 msFirebolt46.7 msDataFusion93.2 msClickHouse78.8 ms
  7. 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 Mateo Ruiz (analyst for the VP Customer Experience):
      
      Title: CSAT score by ticket resolution-time bucket
      Question: Does a slower ticket resolution actually translate into a lower satisfaction score?
      Grain: one row per resolution-time bucket, platform-wide, across all tickets 2023-2025 that have both a resolution and a CSAT response
      Columns:
      - resolution time bucket
      - tickets (count)
      - avg CSAT score (1-5)
      - % satisfied (score >= 4)
      - % dissatisfied (score <= 2)
      Details: Source: tickets joined to csat_responses on tickets.id = csat_responses.ticket_id. Only include tickets with resolved_at not null and a matching csat_responses row. resolution_hours = extract(epoch from resolved_at - created_at)/3600. Bucket into: '<4h', '4-24h', '1-3 days' (24-72h), '3-7 days' (72-168h), '>7 days' (168h+). Window: tickets.created_at 2023-01-01 through 2025-12-31 (full history, to get a reliable sample size per bucket). avg score over csat_responses.score for tickets in that bucket, rounded to 2 decimals. '% satisfied'/'% dissatisfied' as in the other CSAT reports, rounded to 1 decimal. Order by the bucket's natural time order (use a numeric sort key for the lower bound of each bucket, e.g. 0, 4, 24, 72, 168), not alphabetically. Expect exactly 5 rows.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Grace calls dry_run
      with resolved_tickets_with_csat as (
          select
              t.id as ticket_id,
              c.score as csat_score,
              extract(epoch from (t.resolved_at - t.created_at)) / 3600.0 as resolution_hours
          from tickets t
          join csat_responses c
              on c.ticket_id = t.id
          where t.resolved_at is not null
              and t.created_at >= timestamp '2023-01-01'
              and t.created_at < timestamp '2026-01-01'
      ),
      bucketed_tickets as (
          select
              ticket_id,
              csat_score,
              case
                  when resolution_hours < 4 then '<4h'
                  when resolution_hours < 24 then '4-24h'
                  when resolution_hours < 72 then '1-3 days'
                  when resolution_hours < 168 then '3-7 days'
                  else '>7 days'
              end as resolution_bucket,
              case
                  when resolution_hours < 4 then 0
                  when resolution_hours < 24 then 4
                  when resolution_hours < 72 then 24
                  when resolution_hours < 168 then 72
                  else 168
              end as bucket_sort_key
          from resolved_tickets_with_csat
      ),
      bucket_summary as (
          select
              resolution_bucket,
              bucket_sort_key,
              count(*) as ticket_count,
              round(avg(csat_score)::numeric, 2) as avg_csat_score,
              round(100.0 * sum(case when csat_score >= 4 then 1 else 0 end) / count(*), 1) as pct_satisfied,
              round(100.0 * sum(case when csat_score <= 2 then 1 else 0 end) / count(*), 1) as pct_dissatisfied
          from bucketed_tickets
          group by resolution_bucket, bucket_sort_key
      )
      select
          resolution_bucket as resolution_time_bucket,
          ticket_count as tickets,
          avg_csat_score as avg_csat_score,
          pct_satisfied as pct_satisfied,
          pct_dissatisfied as pct_dissatisfied
      from bucket_summary
      order by bucket_sort_key
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: resolution_time_bucket, tickets, avg_csat_score, pct_satisfied, pct_dissatisfied
    • GraceQuery compiles cleanly and returns the expected 5 columns at the resolution-bucket grain.
    • Grace calls submit
      with resolved_tickets_with_csat as (
          select
              t.id as ticket_id,
              c.score as csat_score,
              extract(epoch from (t.resolved_at - t.created_at)) / 3600.0 as resolution_hours
          from tickets t
          join csat_responses c
              on c.ticket_id = t.id
          where t.resolved_at is not null
              and t.created_at >= timestamp '2023-01-01'
              and t.created_at < timestamp '2026-01-01'
      ),
      bucketed_tickets as (
          select
              ticket_id,
              csat_score,
              case
                  when resolution_hours < 4 then '<4h'
                  when resolution_hours < 24 then '4-24h'
                  when resolution_hours < 72 then '1-3 days'
                  when resolution_hours < 168 then '3-7 days'
                  else '>7 days'
              end as resolution_bucket,
              case
                  when resolution_hours < 4 then 0
                  when resolution_hours < 24 then 4
                  when resolution_hours < 72 then 24
                  when resolution_hours < 168 then 72
                  else 168
              end as bucket_sort_key
          from resolved_tickets_with_csat
      ),
      bucket_summary as (
          select
              resolution_bucket,
              bucket_sort_key,
              count(*) as ticket_count,
              round(avg(csat_score)::numeric, 2) as avg_csat_score,
              round(100.0 * sum(case when csat_score >= 4 then 1 else 0 end) / count(*), 1) as pct_satisfied,
              round(100.0 * sum(case when csat_score <= 2 then 1 else 0 end) / count(*), 1) as pct_dissatisfied
          from bucketed_tickets
          group by resolution_bucket, bucket_sort_key
      )
      select
          resolution_bucket as resolution_time_bucket,
          ticket_count as tickets,
          avg_csat_score as avg_csat_score,
          pct_satisfied as pct_satisfied,
          pct_dissatisfied as pct_dissatisfied
      from bucket_summary
      order by bucket_sort_key
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • GraceSubmitted. The report buckets all 2023–2025 tickets with both a resolution and a CSAT response into the five requested resolution-time ranges, with ticket counts, average CSAT, and % satisfied/dissatisfied per bucket, ordered by increasing resolution time.