DB Olympics

Queries · q007 · Chief Executive Officer · week 1, night 3

Revenue by vertical, trailing 12 months (corrected)

“Which merchant verticals generate the bulk of Tidewick's processing volume and fee revenue today? (redo of night-1 report, which disagreed with the merchant-level total by ~1.76x)”

Manager · asked the questionMaren OkaforChief Executive Officer. Joined two weeks ago from a European payments company. Board meeting at the end of January.
Analyst · planned the reportHannah BrooksMethodical: starts with an overview, then drills into whatever looks unusual.
Engineer · wrote the SQLOmarPragmatic: the shortest correct query, aggregates first and joins dimensions afterwards, FILTER clauses for conditional counts.

Fictional people at a fictional company, each played by a language-model agent. Who they are and what they are told

  1. Maren Okafor writes to Hannah Brooks

    The manager, at the start of week 1

    Subject: A few questions to get me oriented before the board meeting

    Hi Hannah,

    Thanks for taking these on this week — I know slots are tight, so please prioritize in the order below and don't force everything into this week if it doesn't fit cleanly. I'd rather get three solid answers than five rushed ones.

    1. Where does our revenue actually come from? I'd like a sense of how concentrated we are — which merchants and which verticals are driving the bulk of our volume and fees today, and how that's shifted over the last year or so. I'm trying to understand if we're overly dependent on a handful of accounts.
    2. How healthy is our growth? Are we gaining merchants and transaction volume, or is it flat/declining? I'd like to see the trend over the last several quarters, and ideally whether growth is coming from new merchants or from existing ones doing more.
    3. Where do we make our money, and how is that mix changing? Between platform fees and payment processing, I want to understand the balance, and whether one is growing faster than the other.
    4. What's working and what's not, vertical by vertical? I'd like a simple read on which verticals are thriving versus struggling — whether that's measured by growth, retention, or merchant satisfaction, whatever tells the clearest story.
    5. (If time allows) Are there any warning signs I should know about — merchants at risk of leaving, payment or fulfillment problems, support issues — anything that could become a board-level concern if it's not already on my radar.

    What I need this for: I'm building my mental model of the business ahead of the board meeting at the end of January, and I want my first real conversation with the board to show I understand where we're strong, where we're exposed, and what I'm watching closely. Your Friday memo will be the backbone of that.

    Let me know if any of these need narrowing once you've had a look at what's feasible — happy to discuss before you lock in the report requests.

    Thanks, Maren

  2. Hannah Brooks plans the night

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

    The reconciliation confirms the correct answer: payouts.status only has two values, 'paid' (fully settled, paid_at always populated) and 'in_transit' (future-dated, paid_at always null). Filtering status='paid' with paid_at in [2025-01-01, 2026-01-01) gives fee revenue of €53,185.85 for the trailing 12 months — matching night 1's merchant-level report almost exactly. That means the merchant-concentration report (Report 1, night 1) was correct, and the vertical-level report (Report 3, night 1, €93.6k) has a bug, most likely a join fan-out against exchange_rates or tenants. I'm having that redone first since it's a wrong number already in front of me. I also want to note for Maren: I don't see any table in the catalog that represents Tidewick's own monthly platform-plan billing of merchants — billing/subscriptions/invoices tables are all tenant-scoped to the merchant's own business (their customers' subscriptions), not Tidewick-to-merchant billing. So priority #3 (platform fees vs. processing mix) likely can't be answered from this warehouse as a two-sided split; I'll reframe it around the processing take-rate trend, which is measurable, and flag the gap. Third slot starts priority #4 (vertical health) with a YoY growth view by vertical.

    Revenue by vertical, trailing 12 months (corrected)
    Question
    Which merchant verticals generate the bulk of Tidewick's processing volume and fee revenue today? (redo of night-1 report, which disagreed with the merchant-level total by ~1.76x)
    One row per
    merchant vertical (tenants.industry)
    Columns
    • vertical (industry)
    • merchant count (distinct tenants with a qualifying payout)
    • gross payment volume (EUR)
    • fee revenue (EUR)
    • average fee revenue per merchant (EUR)
    • share of total fee revenue (%)
  3. Omar 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 payout_eur AS (
      SELECT p.tenant_id,
             p.gross_amount / er.rate AS gross_eur,
             p.fee_amount / er.rate AS fee_eur
      FROM payouts p
      JOIN exchange_rates er
        ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = date(p.paid_at)
      WHERE p.status = 'paid' AND p.paid_at >= DATE '2025-01-01' AND p.paid_at < DATE '2026-01-01'
    ),
    tenant_agg AS (
      SELECT tenant_id, SUM(gross_eur) AS gross_eur, SUM(fee_eur) AS fee_eur
      FROM payout_eur
      GROUP BY tenant_id
    )
    SELECT
      COALESCE(NULLIF(TRIM(t.industry), ''), 'Unspecified') AS vertical,
      COUNT(DISTINCT ta.tenant_id) AS merchant_count,
      ROUND(SUM(ta.gross_eur)::numeric,2) AS gross_payment_volume_eur,
      ROUND(SUM(ta.fee_eur)::numeric,2) AS fee_revenue_eur,
      ROUND((SUM(ta.fee_eur) / COUNT(DISTINCT ta.tenant_id))::numeric,2) AS avg_fee_revenue_per_merchant_eur,
      ROUND((100.0 * SUM(ta.fee_eur) / SUM(SUM(ta.fee_eur)) OVER ())::numeric,2) AS share_of_total_fee_revenue_pct
    FROM tenant_agg ta
    JOIN tenants t ON t.id = ta.tenant_id
    GROUP BY vertical
    ORDER BY fee_revenue_eur DESC, vertical ASC
    Submitted · 1 change from the dry run before · Submitted to tonight's batch. Your work on this request is done.
    WITH payout_eur AS (  SELECT p.tenant_id,         p.gross_amount / er.rate AS gross_eur,         p.fee_amount / er.rate AS fee_eur  FROM payouts p  JOIN exchange_rates er    ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = date(p.paid_at)  WHERE p.status = 'paid' AND p.paid_at >= DATE '2025-01-01' AND p.paid_at < DATE '2026-01-01'),tenant_agg AS (  SELECT tenant_id, SUM(gross_eur) AS gross_eur, SUM(fee_eur) AS fee_eur  FROM payout_eur  GROUP BY tenant_id)SELECT  COALESCE(NULLIF(TRIM(t.industry), ''), 'Unspecified') AS vertical,  COUNT(DISTINCT ta.tenant_id) AS merchant_count,  ROUND(SUM(ta.gross_eur)::numeric,2) AS gross_payment_volume_eur,  ROUND(SUM(ta.fee_eur)::numeric,2) AS fee_revenue_eur,  ROUND((SUM(ta.fee_eur) / COUNT(DISTINCT ta.tenant_id))::numeric,2) AS avg_fee_revenue_per_merchant_eur,  ROUND((100.0 * SUM(ta.fee_eur) / SUM(SUM(ta.fee_eur)) OVER ())::numeric,2) AS share_of_total_fee_revenue_pct  ROUND(SUM(ta.gross_eur)::numeric, 2) AS gross_payment_volume_eur,  ROUND(SUM(ta.fee_eur)::numeric, 2) AS fee_revenue_eur,  ROUND((SUM(ta.fee_eur) / COUNT(DISTINCT ta.tenant_id))::numeric, 2) AS avg_fee_revenue_per_merchant_eur,  ROUND((100.0 * SUM(ta.fee_eur) / SUM(SUM(ta.fee_eur)) OVER ())::numeric, 2) AS share_of_total_fee_revenue_pctFROM tenant_agg taJOIN tenants t ON t.id = ta.tenant_idGROUP BY verticalORDER BY fee_revenue_eur DESC, vertical ASC

    “Submitted. Summary of what the query does: - Converts each qualifying payout (status='paid', paid_at in 2025) to EUR individually by joining exchange_rates on base_code='EUR', quote_code=payouts.currency_code, as_of=date(paid_at) — before any aggregation, avoiding the prior fan-out bug. - Aggregates gross/fee EUR per tenant_id first (CTE tenant_agg), then joins to tenants and groups by industry (nulls/blanks bucketed as 'Unspecified'). - Reports: vertical, merchant count, gross volume (EUR), fee revenue (EUR), average fee per merchant, and share of total fee revenue — ordered by fee revenue descending, tie-broken by vertical name. One caveat flagged in the submit note: since dry-run never reads real data, I couldn't personally confirm the grand total lands at ~EUR 53,186 before handing it off — worth a quick cross-check once the report prints tonight.”

  4. The report comes back overnight

    As Hannah Brooks saw it, from the small dataset the agents worked on

    vertical                  | merchant_count | gross_payment_volume_eur | fee_revenue_eur | avg_fee_revenue_per_merchant_eur | share_of_total_fee_revenue_pct
    --------------------------+----------------+--------------------------+-----------------+----------------------------------+-------------------------------
    Apparel & Footwear        |             12 |               1070404.44 |        29028.10 |                          2419.01 |                          54.58
    Sports & Outdoor          |              5 |                209893.74 |         5796.45 |                          1159.29 |                          10.90
    Office Supplies (B2B)     |              2 |                176549.46 |         4592.96 |                          2296.48 |                           8.64
    Industrial Supplies (B2B) |              1 |                107753.57 |         3499.84 |                          3499.84 |                           6.58
    Home & Living             |              9 |                128877.56 |         3085.55 |                           342.84 |                           5.80
    Food & Coffee             |              5 |                 77170.34 |         2597.96 |                           519.59 |                           4.89
    Beauty                    |              8 |                125191.99 |         2346.81 |                           293.35 |                           4.41
    Pet Supplies              |              7 |                 55346.78 |         1468.94 |                           209.85 |                           2.76
    Consumer Electronics      |              3 |                 31713.49 |          529.66 |                           176.55 |                           1.00
    Books & Stationery        |              4 |                  5565.70 |          145.03 |                            36.26 |                           0.27
    Jewelry & Accessories     |              1 |                  2982.02 |           94.56 |                            94.56 |                           0.18
    (11 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:

    It builds on:

  6. It becomes benchmark query q007

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

    • spellingcast (4×)
    • spellingdate cast
    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision; bare NUMERIC cast that only served the rounding removed
    • 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
    • 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 payout_eur AS (  SELECT p.tenant_id,         p.gross_amount / er.rate AS gross_eur,         p.fee_amount / er.rate AS fee_eur  SELECT p.tenant_id AS tenant_id,         CAST(p.gross_amount / er.rate AS NUMERIC(18,6)) AS gross_eur,         CAST(p.fee_amount / er.rate AS NUMERIC(18,6)) AS fee_eur  FROM payouts p  JOIN exchange_rates er    ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = date(p.paid_at)    ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = CAST(p.paid_at AS DATE)  WHERE p.status = 'paid' AND p.paid_at >= DATE '2025-01-01' AND p.paid_at < DATE '2026-01-01'),tenant_agg AS (  SELECT tenant_id, SUM(gross_eur) AS gross_eur, SUM(fee_eur) AS fee_eur  FROM payout_eur  GROUP BY tenant_id)SELECT  COALESCE(NULLIF(TRIM(t.industry), ''), 'Unspecified') AS vertical,  COUNT(DISTINCT ta.tenant_id) AS merchant_count,  ROUND(SUM(ta.gross_eur)::numeric, 2) AS gross_payment_volume_eur,  ROUND(SUM(ta.fee_eur)::numeric, 2) AS fee_revenue_eur,  ROUND((SUM(ta.fee_eur) / COUNT(DISTINCT ta.tenant_id))::numeric, 2) AS avg_fee_revenue_per_merchant_eur,  ROUND((100.0 * SUM(ta.fee_eur) / SUM(SUM(ta.fee_eur)) OVER ())::numeric, 2) AS share_of_total_fee_revenue_pct  SUM(ta.gross_eur) AS gross_payment_volume_eur,  SUM(ta.fee_eur) AS fee_revenue_eur,  (SUM(ta.fee_eur) / COUNT(DISTINCT ta.tenant_id)) AS avg_fee_revenue_per_merchant_eur,  (100.0 * SUM(ta.fee_eur) / SUM(SUM(ta.fee_eur)) OVER ()) AS share_of_total_fee_revenue_pctFROM tenant_agg taJOIN tenants t ON t.id = ta.tenant_idGROUP BY verticalORDER BY fee_revenue_eur DESC, vertical ASCORDER BY fee_revenue_eur DESC NULLS FIRST, vertical ASC NULLS LAST
    WITH payout_eur AS (
      SELECT p.tenant_id AS tenant_id,
             CAST(p.gross_amount / er.rate AS NUMERIC(18,6)) AS gross_eur,
             CAST(p.fee_amount / er.rate AS NUMERIC(18,6)) AS fee_eur
      FROM payouts p
      JOIN exchange_rates er
        ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = CAST(p.paid_at AS DATE)
      WHERE p.status = 'paid' AND p.paid_at >= DATE '2025-01-01' AND p.paid_at < DATE '2026-01-01'
    ),
    tenant_agg AS (
      SELECT tenant_id, SUM(gross_eur) AS gross_eur, SUM(fee_eur) AS fee_eur
      FROM payout_eur
      GROUP BY tenant_id
    )
    SELECT
      COALESCE(NULLIF(TRIM(t.industry), ''), 'Unspecified') AS vertical,
      COUNT(DISTINCT ta.tenant_id) AS merchant_count,
      SUM(ta.gross_eur) AS gross_payment_volume_eur,
      SUM(ta.fee_eur) AS fee_revenue_eur,
      (SUM(ta.fee_eur) / COUNT(DISTINCT ta.tenant_id)) AS avg_fee_revenue_per_merchant_eur,
      (100.0 * SUM(ta.fee_eur) / SUM(SUM(ta.fee_eur)) OVER ()) AS share_of_total_fee_revenue_pct
    FROM tenant_agg ta
    JOIN tenants t ON t.id = ta.tenant_id
    GROUP BY vertical
    ORDER BY fee_revenue_eur DESC NULLS FIRST, vertical ASC NULLS LAST
    10 ms100 msHyper18.3 msDuckDB10.4 msGizmoSQL12.9 msclickhouse_brew70.8 msFirebolterrorDataFusion54.5 msClickHouseerror
    • Firebolt{ "errors": [ { "description": "Line 3, Column 30: 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": "76d94370-6ec9-46b8-a3a0-2eee5ef3fad1", "query_labe
    • ClickHouseCode: 69. DB::Exception: Decimal result's scale is less than argument's one: while executing function divide on arguments __table3.gross_amount Decimal(14, 4) Decimal64(size = 0), __table4.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

    Omar's whole session, 6 turns
    • The request, as Omar received it
      Report request from Hannah Brooks (analyst for the Chief Executive Officer):
      
      Title: Revenue by vertical, trailing 12 months (corrected)
      Question: Which merchant verticals generate the bulk of Tidewick's processing volume and fee revenue today? (redo of night-1 report, which disagreed with the merchant-level total by ~1.76x)
      Grain: one row per merchant vertical (tenants.industry)
      Columns:
      - vertical (industry)
      - merchant count (distinct tenants with a qualifying payout)
      - gross payment volume (EUR)
      - fee revenue (EUR)
      - average fee revenue per merchant (EUR)
      - share of total fee revenue (%)
      Details: Source: payouts only, filtered to status='paid' and paid_at >= 2025-01-01 and paid_at < 2026-01-01 (matches the window validated in last night's reconciliation report, which found total fee revenue of EUR 53,185.85 for this exact filter). To avoid the fan-out bug suspected in the prior version: first aggregate payouts per tenant_id (sum gross_amount and fee_amount, converting each payout row to EUR individually via exchange_rates where base_code='EUR', quote_code=payouts.currency_code, as_of = date(payouts.paid_at) — this should be a 1-row match per payout given the (base_code,quote_code,as_of) unique constraint), and only THEN join that per-tenant aggregate to tenants to pick up industry and group by industry. Please confirm in the report notes that the grand total fee revenue across all vertical rows sums to approximately EUR 53,186 (if it doesn't, something is still wrong and should be flagged rather than printed). Group any tenant with null/blank industry into a single 'Unspecified' row. Order by fee revenue EUR descending, tie-break by vertical name ascending.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Omar calls dry_run
      WITH payout_eur AS (
        SELECT p.tenant_id,
               p.gross_amount / er.rate AS gross_eur,
               p.fee_amount / er.rate AS fee_eur
        FROM payouts p
        JOIN exchange_rates er
          ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = date(p.paid_at)
        WHERE p.status = 'paid' AND p.paid_at >= DATE '2025-01-01' AND p.paid_at < DATE '2026-01-01'
      ),
      tenant_agg AS (
        SELECT tenant_id, SUM(gross_eur) AS gross_eur, SUM(fee_eur) AS fee_eur
        FROM payout_eur
        GROUP BY tenant_id
      )
      SELECT
        COALESCE(NULLIF(TRIM(t.industry), ''), 'Unspecified') AS vertical,
        COUNT(DISTINCT ta.tenant_id) AS merchant_count,
        ROUND(SUM(ta.gross_eur)::numeric,2) AS gross_payment_volume_eur,
        ROUND(SUM(ta.fee_eur)::numeric,2) AS fee_revenue_eur,
        ROUND((SUM(ta.fee_eur) / COUNT(DISTINCT ta.tenant_id))::numeric,2) AS avg_fee_revenue_per_merchant_eur,
        ROUND((100.0 * SUM(ta.fee_eur) / SUM(SUM(ta.fee_eur)) OVER ())::numeric,2) AS share_of_total_fee_revenue_pct
      FROM tenant_agg ta
      JOIN tenants t ON t.id = ta.tenant_id
      GROUP BY vertical
      ORDER BY fee_revenue_eur DESC, vertical ASC
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: vertical, merchant_count, gross_payment_volume_eur, fee_revenue_eur, avg_fee_revenue_per_merchant_eur, share_of_total_fee_revenue_pct
    • Omar calls submit
      WITH payout_eur AS (
        SELECT p.tenant_id,
               p.gross_amount / er.rate AS gross_eur,
               p.fee_amount / er.rate AS fee_eur
        FROM payouts p
        JOIN exchange_rates er
          ON er.base_code = 'EUR' AND er.quote_code = p.currency_code AND er.as_of = date(p.paid_at)
        WHERE p.status = 'paid' AND p.paid_at >= DATE '2025-01-01' AND p.paid_at < DATE '2026-01-01'
      ),
      tenant_agg AS (
        SELECT tenant_id, SUM(gross_eur) AS gross_eur, SUM(fee_eur) AS fee_eur
        FROM payout_eur
        GROUP BY tenant_id
      )
      SELECT
        COALESCE(NULLIF(TRIM(t.industry), ''), 'Unspecified') AS vertical,
        COUNT(DISTINCT ta.tenant_id) AS merchant_count,
        ROUND(SUM(ta.gross_eur)::numeric, 2) AS gross_payment_volume_eur,
        ROUND(SUM(ta.fee_eur)::numeric, 2) AS fee_revenue_eur,
        ROUND((SUM(ta.fee_eur) / COUNT(DISTINCT ta.tenant_id))::numeric, 2) AS avg_fee_revenue_per_merchant_eur,
        ROUND((100.0 * SUM(ta.fee_eur) / SUM(SUM(ta.fee_eur)) OVER ())::numeric, 2) AS share_of_total_fee_revenue_pct
      FROM tenant_agg ta
      JOIN tenants t ON t.id = ta.tenant_id
      GROUP BY vertical
      ORDER BY fee_revenue_eur DESC, vertical ASC
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • OmarSubmitted. Summary of what the query does: - Converts each qualifying payout (`status='paid'`, `paid_at` in 2025) to EUR individually by joining `exchange_rates` on `base_code='EUR'`, `quote_code=payouts.currency_code`, `as_of=date(paid_at)` — before any aggregation, avoiding the prior fan-out bug. - Aggregates gross/fee EUR per `tenant_id` first (CTE `tenant_agg`), then joins to `tenants` and groups by `industry` (nulls/blanks bucketed as `'Unspecified'`). - Reports: vertical, merchant count, gross volume (EUR), fee revenue (EUR), average fee per merchant, and share of total fee revenue — ordered by fee revenue descending, tie-broken by vertical name. One caveat flagged in the submit note: since dry-run never reads real data, I couldn't personally confirm the grand total lands at ~EUR 53,186 before handing it off — worth a quick cross-check once the report prints tonight.