DB Olympics

Queries · q020 · Chief Financial Officer · week 1, night 3

Company-Wide Refund Rate Trend by Quarter, 2024-2025

“Is Tidewick's overall refund rate rising, falling, or flat over the last two years, and does it spike around the Nov/Dec peak-volume period?”

Manager · asked the questionTobias LindqvistChief Financial Officer. New this month, previously finance director at a retailer. Preparing the 2025 annual accounts and the 2026 budget.
Analyst · planned the reportKofi MensahHypothesis-driven: states what he expects and designs reports that would prove him wrong.
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. Tobias Lindqvist writes to Kofi Mensah

    The manager, at the start of week 1

    Subject: Finding my bearings – first set of questions for the week

    Hi Kofi,

    Good to be working with you. I'm still building my mental map of the business, so I'd rather ask a few broad questions well than a lot of narrow ones badly. Given the slot budget, here's what I'd prioritize this week, roughly in order:

    1. How did we actually do in 2025, and where is the money coming from? I want to see our own revenue split between the subscription/plan fees and the payment-processing fees, over the year, and ideally how that's trending month to month. This is the backbone of the annual accounts narrative and will shape how I talk about 2025 to the board.
    2. How concentrated is our business in a few large merchants, and how healthy are they? I keep hearing "a handful of large merchants drive most of the volume" — I'd like to understand who they are, how much of our gross volume and revenue they represent, and whether that concentration is growing or shrinking. This matters for risk assessment going into the budget.
    3. What do refunds and returns look like across the merchant base? I want a sense of refund rates — overall and whether certain verticals or merchants are outliers — since this affects both our net revenue and how much cash we actually end up paying out.
    4. How exposed are we to currency movements? Merchants bill in their own currencies, and shoppers come from ~40 countries. I'd like a picture of how our volume and revenue break down by currency, so I can gauge our FX exposure for the 2026 budget and think about whether we need any hedging conversation.
    5. Are our B2B merchants paying us and their own customers on time? I know a few of our merchants are wholesalers running on B2B terms. I'd like to understand how their receivables look — how much is outstanding and how overdue it tends to get — since this could be a real cash-flow risk if it's worse than I'd expect.

    For this week, let's focus on #1 and #2 — those are the two I most need before I can speak credibly about 2025 and the 2026 budget. If there's room, #3 would be next; #4 and #5 can wait for a follow-up week if slots are tight.

    Let me know if any of these need me to narrow scope further before you brief the data engineers.

    Thanks, Tobias

  2. Kofi Mensah plans the night

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

    The currency reconciliation report is the key finding tonight: payout_count_missing_fx_rate is 0 for every currency, so FX coverage is complete — my "inner join dropping unmatched currencies" hypothesis was wrong. But summing the reconciliation report's own gross_volume_eur across currencies gives €3,972,988 and fee_revenue_eur gives €93,583 — both match Night 1's Report 3 total almost to the cent. That confirms the TRUE 2025 totals, and proves Night 1's Report 1 (monthly trend: €1.99M gross / €53.2k fee) was wrong by a different mechanism than I guessed — not missing FX matches, but something in how that specific query aggregated/converted by month (gross was off by ~2.0x, fee only by ~1.76x, so it's not a uniform "half the rows missing" bug either, more likely a currency-conversion-direction or grouping error localized to that query). Rather than guess further, I'm asking for the monthly report to be rebuilt using the exact validated per-payout conversion method from tonight's reconciliation report, with an explicit annual TOTAL row so it self-checks against the €3,972,988 / €93,583 figures I now trust. This is the top priority tonight since it underpins the board-facing 2025 revenue narrative.

    On refunds: Apparel & Footwear stands out hard at ~20.8% refund rate, 3-6x every other vertical, and it's broad-based across apparel merchants (not just one outlier per Report 3), so I don't think it's a data artifact — apparel/fashion e-commerce genuinely runs high return rates. Still, since this affects net revenue and cash payouts (Tobias's framing), I want to know if it's stable or worsening, especially given the Nov/Dec volume spike we saw in gross volume. I'm using my remaining two slots for a company-wide quarterly refund-rate trend and an Apparel-specific quarterly trend, both 2024 vs 2025, to see direction of travel before I write the memo.

    Company-Wide Refund Rate Trend by Quarter, 2024-2025
    Question
    Is Tidewick's overall refund rate rising, falling, or flat over the last two years, and does it spike around the Nov/Dec peak-volume period?
    One row per
    one calendar quarter (2024-Q1 through 2025-Q4)
    Columns
    • quarter (e.g. 2025-Q4)
    • payment_count
    • gross_captured_eur
    • amount_refunded_eur
    • refund_rate_pct
    • refund_rate_same_quarter_prior_year_pct
  3. 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 t0 AS (
      SELECT
        p.id AS payment_id,
        p.captured_at,
        p.amount,
        p.amount_refunded,
        p.currency_code,
        CAST(EXTRACT(YEAR FROM p.captured_at) AS integer) AS cap_year,
        CAST(EXTRACT(QUARTER FROM p.captured_at) AS integer) AS cap_quarter
      FROM payments p
      WHERE p.status IN ('captured', 'refunded', 'partially_refunded')
        AND p.captured_at >= DATE '2024-01-01'
        AND p.captured_at < DATE '2026-01-01'
    ),
    t1 AS (
      SELECT
        t0.payment_id AS payment_id,
        t0.cap_year AS cap_year,
        t0.cap_quarter AS cap_quarter,
        t0.amount / t0e.rate AS amount_eur,
        t0.amount_refunded / t0e.rate AS amount_refunded_eur
      FROM t0
      JOIN exchange_rates t0e
        ON t0e.base_code = 'EUR'
       AND t0e.quote_code = t0.currency_code
       AND t0e.as_of = CAST(t0.captured_at AS date)
    ),
    t2 AS (
      SELECT
        t1.cap_year AS quarter_year,
        t1.cap_quarter AS quarter_num,
        COUNT(*) AS payment_count,
        SUM(t1.amount_eur) AS gross_captured_eur,
        SUM(t1.amount_refunded_eur) AS amount_refunded_eur
      FROM t1
      GROUP BY t1.cap_year, t1.cap_quarter
    )
    SELECT
      CONCAT(CAST(t2.quarter_year AS varchar), '-Q', CAST(t2.quarter_num AS varchar)) AS quarter,
      t2.payment_count AS payment_count,
      ROUND(t2.gross_captured_eur, 2) AS gross_captured_eur,
      ROUND(t2.amount_refunded_eur, 2) AS amount_refunded_eur,
      ROUND(t2.amount_refunded_eur / NULLIF(t2.gross_captured_eur, 0) * 100, 2) AS refund_rate_pct,
      ROUND(t4.amount_refunded_eur / NULLIF(t4.gross_captured_eur, 0) * 100, 2) AS refund_rate_same_quarter_prior_year_pct
    FROM t2
    LEFT JOIN t2 t4
      ON t4.quarter_year = t2.quarter_year - 1
     AND t4.quarter_num = t2.quarter_num
    ORDER BY t2.quarter_year ASC, t2.quarter_num ASC

    “The query compiles and returns exactly the requested columns. Submitting now.”

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

    “Submitted. The report computes, per calendar quarter from 2024-Q1 through 2025-Q4, company-wide payment counts and EUR-converted gross/refunded amounts from payments (status in captured/refunded/partially_refunded), with the refund rate and its year-over-year comparison. Note: the prior-year column is naturally null for all of 2024 since the source window only starts 2024-01-01 (no 2023 comparison data was pulled), which matches the request's expectation.”

  4. The report comes back overnight

    As Kofi Mensah saw it, from the small dataset the agents worked on

    quarter | payment_count | gross_captured_eur | amount_refunded_eur | refund_rate_pct | refund_rate_same_quarter_prior_year_pct
    --------+---------------+--------------------+---------------------+-----------------+----------------------------------------
    2024-Q1 |          1753 |           443060.0 |            46897.52 |           10.58 |                                    NULL
    2024-Q2 |          1890 |           443690.4 |            46481.96 |           10.48 |                                    NULL
    2024-Q3 |          1956 |          540910.23 |            45449.84 |             8.4 |                                    NULL
    2024-Q4 |          3484 |          799816.05 |            113006.0 |           14.13 |                                    NULL
    2025-Q1 |          2429 |          694589.94 |            53953.48 |            7.77 |                                   10.58
    2025-Q2 |          2417 |          605811.02 |            64609.26 |           10.66 |                                   10.48
    2025-Q3 |          2525 |          582250.66 |            78289.11 |           13.45 |                                     8.4
    2025-Q4 |          4269 |          928304.74 |           110674.65 |           11.92 |                                   14.13
    (8 rows)
  5. What came of it

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

    Later queries that build on this report:

  6. It becomes benchmark query q020

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

    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision
    • numericsdocs/NUMERICS.md: x / exchange rate cast to NUMERIC(18,6) (2×)
    • made explicita selected t.col gets AS col, its standard name made explicit (4×)
    • determinismthe standard leaves the place of NULLs in a sort to the implementation: NULLS LAST ascending, NULLS FIRST descending, as in the reference results (2×)
    The benchmark text, against the submitted SQL
    WITH t0 AS (  SELECT    p.id AS payment_id,    p.captured_at,    p.amount,    p.amount_refunded,    p.currency_code,    p.captured_at AS captured_at,    p.amount AS amount,    p.amount_refunded AS amount_refunded,    p.currency_code AS currency_code,    CAST(EXTRACT(YEAR FROM p.captured_at) AS integer) AS cap_year,    CAST(EXTRACT(QUARTER FROM p.captured_at) AS integer) AS cap_quarter  FROM payments p  WHERE p.status IN ('captured', 'refunded', 'partially_refunded')    AND p.captured_at >= DATE '2024-01-01'    AND p.captured_at < DATE '2026-01-01'),t1 AS (  SELECT    t0.payment_id AS payment_id,    t0.cap_year AS cap_year,    t0.cap_quarter AS cap_quarter,    t0.amount / t0e.rate AS amount_eur,    t0.amount_refunded / t0e.rate AS amount_refunded_eur    CAST(t0.amount / t0e.rate AS NUMERIC(18,6)) AS amount_eur,    CAST(t0.amount_refunded / t0e.rate AS NUMERIC(18,6)) AS amount_refunded_eur  FROM t0  JOIN exchange_rates t0e    ON t0e.base_code = 'EUR'   AND t0e.quote_code = t0.currency_code   AND t0e.as_of = CAST(t0.captured_at AS date)),t2 AS (  SELECT    t1.cap_year AS quarter_year,    t1.cap_quarter AS quarter_num,    COUNT(*) AS payment_count,    SUM(t1.amount_eur) AS gross_captured_eur,    SUM(t1.amount_refunded_eur) AS amount_refunded_eur  FROM t1  GROUP BY t1.cap_year, t1.cap_quarter)SELECT  CONCAT(CAST(t2.quarter_year AS varchar), '-Q', CAST(t2.quarter_num AS varchar)) AS quarter,  t2.payment_count AS payment_count,  ROUND(t2.gross_captured_eur, 2) AS gross_captured_eur,  ROUND(t2.amount_refunded_eur, 2) AS amount_refunded_eur,  ROUND(t2.amount_refunded_eur / NULLIF(t2.gross_captured_eur, 0) * 100, 2) AS refund_rate_pct,  ROUND(t4.amount_refunded_eur / NULLIF(t4.gross_captured_eur, 0) * 100, 2) AS refund_rate_same_quarter_prior_year_pct  t2.gross_captured_eur AS gross_captured_eur,  t2.amount_refunded_eur AS amount_refunded_eur,  t2.amount_refunded_eur / NULLIF(t2.gross_captured_eur, 0) * 100 AS refund_rate_pct,  t4.amount_refunded_eur / NULLIF(t4.gross_captured_eur, 0) * 100 AS refund_rate_same_quarter_prior_year_pctFROM t2LEFT JOIN t2 t4  ON t4.quarter_year = t2.quarter_year - 1 AND t4.quarter_num = t2.quarter_numORDER BY t2.quarter_year ASC, t2.quarter_num ASCORDER BY t2.quarter_year ASC NULLS LAST, t2.quarter_num ASC NULLS LAST
    WITH t0 AS (
      SELECT
        p.id AS payment_id,
        p.captured_at AS captured_at,
        p.amount AS amount,
        p.amount_refunded AS amount_refunded,
        p.currency_code AS currency_code,
        CAST(EXTRACT(YEAR FROM p.captured_at) AS integer) AS cap_year,
        CAST(EXTRACT(QUARTER FROM p.captured_at) AS integer) AS cap_quarter
      FROM payments p
      WHERE p.status IN ('captured', 'refunded', 'partially_refunded')
        AND p.captured_at >= DATE '2024-01-01'
        AND p.captured_at < DATE '2026-01-01'
    ),
    t1 AS (
      SELECT
        t0.payment_id AS payment_id,
        t0.cap_year AS cap_year,
        t0.cap_quarter AS cap_quarter,
        CAST(t0.amount / t0e.rate AS NUMERIC(18,6)) AS amount_eur,
        CAST(t0.amount_refunded / t0e.rate AS NUMERIC(18,6)) AS amount_refunded_eur
      FROM t0
      JOIN exchange_rates t0e
        ON t0e.base_code = 'EUR'
       AND t0e.quote_code = t0.currency_code
       AND t0e.as_of = CAST(t0.captured_at AS date)
    ),
    t2 AS (
      SELECT
        t1.cap_year AS quarter_year,
        t1.cap_quarter AS quarter_num,
        COUNT(*) AS payment_count,
        SUM(t1.amount_eur) AS gross_captured_eur,
        SUM(t1.amount_refunded_eur) AS amount_refunded_eur
      FROM t1
      GROUP BY t1.cap_year, t1.cap_quarter
    )
    SELECT
      CONCAT(CAST(t2.quarter_year AS varchar), '-Q', CAST(t2.quarter_num AS varchar)) AS quarter,
      t2.payment_count AS payment_count,
      t2.gross_captured_eur AS gross_captured_eur,
      t2.amount_refunded_eur AS amount_refunded_eur,
      t2.amount_refunded_eur / NULLIF(t2.gross_captured_eur, 0) * 100 AS refund_rate_pct,
      t4.amount_refunded_eur / NULLIF(t4.gross_captured_eur, 0) * 100 AS refund_rate_same_quarter_prior_year_pct
    FROM t2
    LEFT JOIN t2 t4
      ON t4.quarter_year = t2.quarter_year - 1
     AND t4.quarter_num = t2.quarter_num
    ORDER BY t2.quarter_year ASC NULLS LAST, t2.quarter_num ASC NULLS LAST
    10 ms100 ms1 sHyper119 msDuckDB52.2 msGizmoSQL56.2 msclickhouse_brew171 msFirebolterrorDataFusion242 msClickHouseerror
    • Firebolt{ "errors": [ { "description": "Line 20, Column 20: Operations between decimals with different precision and scale is not supported at the moment. Explicitly cast one of the decimals to other decimal precision and scale." } ], "query": { "query_id": "59277940-da6b-4d98-87a4-c78103aafca7", "query_lab
    • ClickHouseCode: 69. DB::Exception: Decimal result's scale is less than argument's one: while executing function divide on arguments __table3.amount Decimal(14, 4) Decimal64(size = 0), __table5.rate Decimal(12, 6) Decimal64(size = 0). (ARGUMENT_OUT_OF_BOUND)
  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

    Bea's whole session, 7 turns
    • The request, as Bea received it
      Report request from Kofi Mensah (analyst for the Chief Financial Officer):
      
      Title: Company-Wide Refund Rate Trend by Quarter, 2024-2025
      Question: Is Tidewick's overall refund rate rising, falling, or flat over the last two years, and does it spike around the Nov/Dec peak-volume period?
      Grain: one calendar quarter (2024-Q1 through 2025-Q4)
      Columns:
      - quarter (e.g. 2025-Q4)
      - payment_count
      - gross_captured_eur
      - amount_refunded_eur
      - refund_rate_pct
      - refund_rate_same_quarter_prior_year_pct
      Details: Source: payments, filtered to status reflecting a captured/refunded/partially_refunded state with captured_at between 2024-01-01 and 2025-12-31. Convert amount and amount_refunded to EUR via exchange_rates (base_code='EUR', quote_code=payments.currency_code, as_of=date(captured_at)), using the same division method validated for payouts. Group by calendar quarter of captured_at across all tenants combined (company-wide, not split by vertical). refund_rate_pct = amount_refunded_eur / gross_captured_eur * 100. The prior-year column repeats the same calculation for the matching quarter one year earlier (null for 2024 rows). Order chronologically 2024-Q1 to 2025-Q4 (8 rows).
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Bea calls dry_run
      WITH t0 AS (
        SELECT
          p.id AS payment_id,
          p.captured_at,
          p.amount,
          p.amount_refunded,
          p.currency_code,
          CAST(EXTRACT(YEAR FROM p.captured_at) AS integer) AS cap_year,
          CAST(EXTRACT(QUARTER FROM p.captured_at) AS integer) AS cap_quarter
        FROM payments p
        WHERE p.status IN ('captured', 'refunded', 'partially_refunded')
          AND p.captured_at >= DATE '2024-01-01'
          AND p.captured_at < DATE '2026-01-01'
      ),
      t1 AS (
        SELECT
          t0.payment_id AS payment_id,
          t0.cap_year AS cap_year,
          t0.cap_quarter AS cap_quarter,
          t0.amount / t0e.rate AS amount_eur,
          t0.amount_refunded / t0e.rate AS amount_refunded_eur
        FROM t0
        JOIN exchange_rates t0e
          ON t0e.base_code = 'EUR'
         AND t0e.quote_code = t0.currency_code
         AND t0e.as_of = CAST(t0.captured_at AS date)
      ),
      t2 AS (
        SELECT
          t1.cap_year AS quarter_year,
          t1.cap_quarter AS quarter_num,
          COUNT(*) AS payment_count,
          SUM(t1.amount_eur) AS gross_captured_eur,
          SUM(t1.amount_refunded_eur) AS amount_refunded_eur
        FROM t1
        GROUP BY t1.cap_year, t1.cap_quarter
      )
      SELECT
        CONCAT(CAST(t2.quarter_year AS varchar), '-Q', CAST(t2.quarter_num AS varchar)) AS quarter,
        t2.payment_count AS payment_count,
        ROUND(t2.gross_captured_eur, 2) AS gross_captured_eur,
        ROUND(t2.amount_refunded_eur, 2) AS amount_refunded_eur,
        ROUND(t2.amount_refunded_eur / NULLIF(t2.gross_captured_eur, 0) * 100, 2) AS refund_rate_pct,
        ROUND(t4.amount_refunded_eur / NULLIF(t4.gross_captured_eur, 0) * 100, 2) AS refund_rate_same_quarter_prior_year_pct
      FROM t2
      LEFT JOIN t2 t4
        ON t4.quarter_year = t2.quarter_year - 1
       AND t4.quarter_num = t2.quarter_num
      ORDER BY t2.quarter_year ASC, t2.quarter_num ASC
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: quarter, payment_count, gross_captured_eur, amount_refunded_eur, refund_rate_pct, refund_rate_same_quarter_prior_year_pct
    • BeaThe query compiles and returns exactly the requested columns. Submitting now.
    • Bea calls submit
      WITH t0 AS (
        SELECT
          p.id AS payment_id,
          p.captured_at,
          p.amount,
          p.amount_refunded,
          p.currency_code,
          CAST(EXTRACT(YEAR FROM p.captured_at) AS integer) AS cap_year,
          CAST(EXTRACT(QUARTER FROM p.captured_at) AS integer) AS cap_quarter
        FROM payments p
        WHERE p.status IN ('captured', 'refunded', 'partially_refunded')
          AND p.captured_at >= DATE '2024-01-01'
          AND p.captured_at < DATE '2026-01-01'
      ),
      t1 AS (
        SELECT
          t0.payment_id AS payment_id,
          t0.cap_year AS cap_year,
          t0.cap_quarter AS cap_quarter,
          t0.amount / t0e.rate AS amount_eur,
          t0.amount_refunded / t0e.rate AS amount_refunded_eur
        FROM t0
        JOIN exchange_rates t0e
          ON t0e.base_code = 'EUR'
         AND t0e.quote_code = t0.currency_code
         AND t0e.as_of = CAST(t0.captured_at AS date)
      ),
      t2 AS (
        SELECT
          t1.cap_year AS quarter_year,
          t1.cap_quarter AS quarter_num,
          COUNT(*) AS payment_count,
          SUM(t1.amount_eur) AS gross_captured_eur,
          SUM(t1.amount_refunded_eur) AS amount_refunded_eur
        FROM t1
        GROUP BY t1.cap_year, t1.cap_quarter
      )
      SELECT
        CONCAT(CAST(t2.quarter_year AS varchar), '-Q', CAST(t2.quarter_num AS varchar)) AS quarter,
        t2.payment_count AS payment_count,
        ROUND(t2.gross_captured_eur, 2) AS gross_captured_eur,
        ROUND(t2.amount_refunded_eur, 2) AS amount_refunded_eur,
        ROUND(t2.amount_refunded_eur / NULLIF(t2.gross_captured_eur, 0) * 100, 2) AS refund_rate_pct,
        ROUND(t4.amount_refunded_eur / NULLIF(t4.gross_captured_eur, 0) * 100, 2) AS refund_rate_same_quarter_prior_year_pct
      FROM t2
      LEFT JOIN t2 t4
        ON t4.quarter_year = t2.quarter_year - 1
       AND t4.quarter_num = t2.quarter_num
      ORDER BY t2.quarter_year ASC, t2.quarter_num ASC
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • BeaSubmitted. The report computes, per calendar quarter from 2024-Q1 through 2025-Q4, company-wide payment counts and EUR-converted gross/refunded amounts from `payments` (status in captured/refunded/partially_refunded), with the refund rate and its year-over-year comparison. Note: the prior-year column is naturally null for all of 2024 since the source window only starts 2024-01-01 (no 2023 comparison data was pulled), which matches the request's expectation.