DB Olympics

Queries · q388 · Head of B2B Commerce · week 2, night 3

B2B Wholesale: Dollar-Weighted vs Count-Weighted Win Rate by Merchant

“Does Youssef Meijer's 'wins small deals, loses the big one' pattern show up at the Polder Packaging merchant level too, or is it fully offset by his co-rep (Thijs Dijkstra) once you zoom out?”

Manager · asked the questionPatrick O'NeillHead of B2B Commerce. Built B2B sales teams at a software company. Owns the B2B features wholesale merchants run their sales on.
Analyst · planned the reportDaniel KimFunnel thinker: measures each step of a process and where things drop out.
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 Head of B2B Commerce's desk

    Week 1; shared with Samuel Whitcombe, Head of Merchant Success

    B2B merchants say their sales teams struggle. How do their pipelines convert from lead to won deal, how long does it take, and what predicts winning or losing?

  2. Patrick O'Neill writes to Daniel Kim

    The manager, at the start of week 2

    Note to Daniel Kim — reactions and next focus

    Daniel, this is a strong memo — a few things surprised me and changed what I want to chase next.

    What surprised me / needs explaining:

    • The "zero open pipeline" finding is bigger than a side note to me — if every opportunity we've ever seen across 3 years is already closed, we have no forward-looking view at all right now for any of these merchants. Before I take that to Samuel, I need to understand whether that's a real business fact (deals genuinely don't sit open long) or whether open deals simply aren't being entered into the system until they're decided.
    • The "lost deals get more attention, not less" finding cuts against my instinct. I want to understand the shape of that attention, not just the average — are reps front-loading effort and then the deal goes quiet for weeks before it's marked lost, or is the activity spread evenly, including right up to the end? That tells us whether the fix is "kill deals faster" or "the last mile of a sale is where we're weak."
    • Youssef Meijer is the one concrete, actionable lead we got this week — I want to go one level deeper before I talk to Polder Packaging.

    Questions for this week:

    1. For the deals that eventually lose, can you show roughly when the activity happens relative to the deal's life — clustered early, steady throughout, or a late flurry right before it's given up on? I want to know if these are "stalled and forgotten until someone finally closes it out" versus "actively fought to the end and still lost."
    2. Is the complete absence of open pipeline a constant feature of the last three years, or has it changed recently — i.e., are merchants currently sitting on live deals that just haven't shown up yet, or does this system genuinely never carry an open deal for more than a few weeks?
    3. Can you pull Youssef Meijer's individual deals — which accounts, what sizes, how old — so I can tell whether this looks like a few bad accounts/territory versus a pattern across all his work?
    4. Of the lost deals, is there any sign of when in the quarter or year they tend to die — e.g., do deals linger longest around certain times (end of quarter pushes, holiday periods), or is the ~35-day drag constant regardless of when the deal started?

    I know the instrumentation gaps (no stage history, no merchant-side payment terms) aren't fixable by you this week — flag both to product as you planned, that's the right move. Let's prioritize Q1 and Q3 above everything else; Q2 and Q4 can slip if slots run short.

    Thanks, Daniel — good detective work this week.

    Patrick

  3. Daniel Kim plans the night

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

    Tonight I'm only spending one of three slots — the other two questions Patrick raised (Q2: is zero open pipeline a freeze or coincidence; Q4: seasonality of lost-deal duration) are already answered well enough from last night's data without needing more queries: (1) opportunity creation in Dec 2025 was low (1 new opp) but within the historical range (monthly creation has dipped to 1-2 before, e.g. Jul 2023, Mar 2024), and the month-end open balance has hit zero before (Jul 2025) — so this reads as normal pipeline volatility for a low-volume process, not a data freeze or ingestion gap; (2) lost-deal duration by quarter bounces between ~18 and ~49 days with only 1-8 lost deals per quarter — there's a mild hint that Q4/holiday-period losses run a bit longer in 2 of 3 years, but the samples are too small to call it a real seasonal effect, so I'll report it as inconclusive rather than overclaim a pattern. The one report I am requesting tonight extends the highest-priority finding (Youssef Meijer loses his biggest deals) up to the merchant level, since Patrick will want to know whether this is purely a one-rep coaching conversation or something visible in Polder Packaging's own numbers before he talks to that merchant. I'm holding night 4 in reserve in case this merchant-level check surfaces something unexpected that needs one more follow-up; otherwise I'm ready to write Friday's memo.

    B2B Wholesale: Dollar-Weighted vs Count-Weighted Win Rate by Merchant
    Question
    Does Youssef Meijer's 'wins small deals, loses the big one' pattern show up at the Polder Packaging merchant level too, or is it fully offset by his co-rep (Thijs Dijkstra) once you zoom out?
    One row per
    wholesale merchant (tenant)
    Columns
    • merchant
    • opportunities_closed
    • count_win_rate_pct
    • total_amount_won_eur
    • total_amount_lost_eur
    • dollar_weighted_win_rate_pct
    • gap_pct_points (count_win_rate_pct minus dollar_weighted_win_rate_pct)
  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 opp AS (
      SELECT
        o.tenant_id,
        t.name AS merchant,
        o.amount,
        o.currency_code,
        o.closed_at,
        os.is_won,
        os.is_lost
      FROM opportunities o
      JOIN tenants t ON t.id = o.tenant_id
      JOIN pipelines p ON p.id = o.pipeline_id AND p.tenant_id = o.tenant_id
      JOIN opportunity_stages os ON os.id = o.stage_id
      WHERE t.name IN (
        'Ballycorran Industrial Supply','Castell Fasteners','Hanse Industriebedarf',
        'Midland Safety Wear','Polder Packaging'
      )
      AND p.name = 'New business'
      AND o.is_closed = true
    ),
    rated AS (
      SELECT
        opp.merchant,
        opp.is_won,
        opp.is_lost,
        CASE
          WHEN opp.currency_code = 'EUR' THEN opp.amount
          ELSE opp.amount / (
            SELECT er.rate
            FROM exchange_rates er
            WHERE er.base_code = 'EUR'
              AND er.quote_code = opp.currency_code
              AND er.as_of <= opp.closed_at::date
            ORDER BY er.as_of DESC
            LIMIT 1
          )
        END AS amount_eur
      FROM opp
    ),
    agg AS (
      SELECT
        merchant,
        COUNT(*) AS opportunities_closed,
        SUM(CASE WHEN is_won THEN 1 ELSE 0 END) AS won_count,
        SUM(CASE WHEN is_won THEN amount_eur ELSE 0 END) AS total_amount_won_eur,
        SUM(CASE WHEN is_lost THEN amount_eur ELSE 0 END) AS total_amount_lost_eur
      FROM rated
      GROUP BY merchant
    )
    SELECT
      merchant,
      opportunities_closed,
      ROUND(100.0 * won_count / opportunities_closed, 2) AS count_win_rate_pct,
      ROUND(total_amount_won_eur, 2) AS total_amount_won_eur,
      ROUND(total_amount_lost_eur, 2) AS total_amount_lost_eur,
      ROUND(100.0 * total_amount_won_eur / NULLIF(total_amount_won_eur + total_amount_lost_eur, 0), 2) AS dollar_weighted_win_rate_pct,
      ROUND(
        (100.0 * won_count / opportunities_closed)
        - (100.0 * total_amount_won_eur / NULLIF(total_amount_won_eur + total_amount_lost_eur, 0)),
        2
      ) AS gap_pct_points
    FROM agg
    ORDER BY gap_pct_points DESC, merchant ASC

    “Query compiles and returns the expected 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 groups the five named wholesale merchants by their own "New business" pipeline's closed opportunities, converts non-EUR amounts using the EUR exchange rate as of (or before) closed_at, and computes both count-weighted and dollar-weighted win rates with the gap between them, ordered by the largest gap first — which is where Youssef's "wins small, loses big" pattern would show up most clearly at the merchant level.”

  5. The report comes back overnight

    As Daniel Kim saw it, from the small dataset the agents worked on

    merchant                      | opportunities_closed | count_win_rate_pct | total_amount_won_eur | total_amount_lost_eur | dollar_weighted_win_rate_pct | gap_pct_points
    ------------------------------+----------------------+--------------------+----------------------+-----------------------+------------------------------+---------------
    Polder Packaging              |                   16 |               75.0 |              58900.0 |               43500.0 |                        57.52 |          17.48
    Castell Fasteners             |                   36 |              86.11 |            163251.62 |              28142.97 |                         85.3 |           0.82
    Ballycorran Industrial Supply |                   76 |              82.89 |             257600.0 |               51200.0 |                        83.42 |          -0.52
    Hanse Industriebedarf         |                   17 |              88.24 |              83300.0 |               10400.0 |                         88.9 |          -0.67
    Midland Safety Wear           |                   40 |               85.0 |             187900.0 |               25800.0 |                        87.93 |          -2.93
    (5 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 q388

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

    • spellingcast
    • spellingfetch first
    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision
    • made explicita selected t.col gets AS col, its standard name made explicit (9×)
    • determinismthe standard leaves the place of NULLs in a sort to the implementation: NULLS LAST ascending, NULLS FIRST descending, as in the reference results (3×)
    • placeholderfetch first
    The benchmark text, against the submitted SQL
    WITH opp AS (  SELECT    o.tenant_id,    o.tenant_id AS tenant_id,    t.name AS merchant,    o.amount,    o.currency_code,    o.closed_at,    os.is_won,    os.is_lost    o.amount AS amount,    o.currency_code AS currency_code,    o.closed_at AS closed_at,    os.is_won AS is_won,    os.is_lost AS is_lost  FROM opportunities o  JOIN tenants t ON t.id = o.tenant_id  JOIN pipelines p ON p.id = o.pipeline_id AND p.tenant_id = o.tenant_id  JOIN opportunity_stages os ON os.id = o.stage_id  WHERE t.name IN (    'Ballycorran Industrial Supply','Castell Fasteners','Hanse Industriebedarf',    'Midland Safety Wear','Polder Packaging'  )  AND p.name = 'New business'  AND o.is_closed = true),rated AS (  SELECT    opp.merchant,    opp.is_won,    opp.is_lost,    opp.merchant AS merchant,    opp.is_won AS is_won,    opp.is_lost AS is_lost,    CASE      WHEN opp.currency_code = 'EUR' THEN opp.amount      ELSE opp.amount / (        SELECT er.rate        FROM exchange_rates er        WHERE er.base_code = 'EUR'          AND er.quote_code = opp.currency_code          AND er.as_of <= opp.closed_at::date        ORDER BY er.as_of DESC        LIMIT 1          AND er.as_of <= CAST(opp.closed_at AS date)        ORDER BY er.as_of DESC NULLS FIRST        {{fetch_first(1)}}      )    END AS amount_eur  FROM opp),agg AS (  SELECT    merchant,    COUNT(*) AS opportunities_closed,    SUM(CASE WHEN is_won THEN 1 ELSE 0 END) AS won_count,    SUM(CASE WHEN is_won THEN amount_eur ELSE 0 END) AS total_amount_won_eur,    SUM(CASE WHEN is_lost THEN amount_eur ELSE 0 END) AS total_amount_lost_eur  FROM rated  GROUP BY merchant)SELECT  merchant,  opportunities_closed,  ROUND(100.0 * won_count / opportunities_closed, 2) AS count_win_rate_pct,  ROUND(total_amount_won_eur, 2) AS total_amount_won_eur,  ROUND(total_amount_lost_eur, 2) AS total_amount_lost_eur,  ROUND(100.0 * total_amount_won_eur / NULLIF(total_amount_won_eur + total_amount_lost_eur, 0), 2) AS dollar_weighted_win_rate_pct,  ROUND(    (100.0 * won_count / opportunities_closed)    - (100.0 * total_amount_won_eur / NULLIF(total_amount_won_eur + total_amount_lost_eur, 0)),    2  ) AS gap_pct_points  100.0 * won_count / opportunities_closed AS count_win_rate_pct,  total_amount_won_eur AS total_amount_won_eur,  total_amount_lost_eur AS total_amount_lost_eur,  100.0 * total_amount_won_eur / NULLIF(total_amount_won_eur + total_amount_lost_eur, 0) AS dollar_weighted_win_rate_pct,  (100.0 * won_count / opportunities_closed)  - (100.0 * total_amount_won_eur / NULLIF(total_amount_won_eur + total_amount_lost_eur, 0)) AS gap_pct_pointsFROM aggORDER BY gap_pct_points DESC, merchant ASCORDER BY gap_pct_points DESC NULLS FIRST, merchant ASC NULLS LAST
    WITH opp AS (
      SELECT
        o.tenant_id AS tenant_id,
        t.name AS merchant,
        o.amount AS amount,
        o.currency_code AS currency_code,
        o.closed_at AS closed_at,
        os.is_won AS is_won,
        os.is_lost AS is_lost
      FROM opportunities o
      JOIN tenants t ON t.id = o.tenant_id
      JOIN pipelines p ON p.id = o.pipeline_id AND p.tenant_id = o.tenant_id
      JOIN opportunity_stages os ON os.id = o.stage_id
      WHERE t.name IN (
        'Ballycorran Industrial Supply','Castell Fasteners','Hanse Industriebedarf',
        'Midland Safety Wear','Polder Packaging'
      )
      AND p.name = 'New business'
      AND o.is_closed = true
    ),
    rated AS (
      SELECT
        opp.merchant AS merchant,
        opp.is_won AS is_won,
        opp.is_lost AS is_lost,
        CASE
          WHEN opp.currency_code = 'EUR' THEN opp.amount
          ELSE opp.amount / (
            SELECT er.rate
            FROM exchange_rates er
            WHERE er.base_code = 'EUR'
              AND er.quote_code = opp.currency_code
              AND er.as_of <= CAST(opp.closed_at AS date)
            ORDER BY er.as_of DESC NULLS FIRST
            FETCH FIRST 1 ROWS ONLY
          )
        END AS amount_eur
      FROM opp
    ),
    agg AS (
      SELECT
        merchant,
        COUNT(*) AS opportunities_closed,
        SUM(CASE WHEN is_won THEN 1 ELSE 0 END) AS won_count,
        SUM(CASE WHEN is_won THEN amount_eur ELSE 0 END) AS total_amount_won_eur,
        SUM(CASE WHEN is_lost THEN amount_eur ELSE 0 END) AS total_amount_lost_eur
      FROM rated
      GROUP BY merchant
    )
    SELECT
      merchant,
      opportunities_closed,
      100.0 * won_count / opportunities_closed AS count_win_rate_pct,
      total_amount_won_eur AS total_amount_won_eur,
      total_amount_lost_eur AS total_amount_lost_eur,
      100.0 * total_amount_won_eur / NULLIF(total_amount_won_eur + total_amount_lost_eur, 0) AS dollar_weighted_win_rate_pct,
      (100.0 * won_count / opportunities_closed)
      - (100.0 * total_amount_won_eur / NULLIF(total_amount_won_eur + total_amount_lost_eur, 0)) AS gap_pct_points
    FROM agg
    ORDER BY gap_pct_points DESC NULLS FIRST, merchant ASC NULLS LAST
    100 ms1 sHyper551 msDuckDB425 msGizmoSQL450 msclickhouse_brew127 msFirebolterrorDataFusionerrorClickHouseerror
    • Firebolt{ "errors": [ { "description": "Line 28, Column 23: 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": "90ef09a7-f81b-486f-9c47-6ff323821e83", "query_lab
    • DataFusionThis feature is not implemented: Physical plan does not support logical expression ScalarSubquery(<subquery>)
    • ClickHouseCode: 48. DB::Exception: Cannot decorrelate query, because 'Limit' step is not supported. (NOT_IMPLEMENTED)
  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 Daniel Kim (analyst for the Head of B2B Commerce):
      
      Title: B2B Wholesale: Dollar-Weighted vs Count-Weighted Win Rate by Merchant
      Question: Does Youssef Meijer's 'wins small deals, loses the big one' pattern show up at the Polder Packaging merchant level too, or is it fully offset by his co-rep (Thijs Dijkstra) once you zoom out?
      Grain: one row per wholesale merchant (tenant)
      Columns:
      - merchant
      - opportunities_closed
      - count_win_rate_pct
      - total_amount_won_eur
      - total_amount_lost_eur
      - dollar_weighted_win_rate_pct
      - gap_pct_points (count_win_rate_pct minus dollar_weighted_win_rate_pct)
      Details: Same methodology as last night's rep-level dollar-weighted report, but grouped by tenant instead of owner_user_id, with no minimum-deal threshold (all 5 merchants have well over 3 closed deals). Scope: tenants.name IN ('Ballycorran Industrial Supply','Castell Fasteners','Hanse Industriebedarf','Midland Safety Wear','Polder Packaging'), pipeline_id = each tenant's 'New business' pipeline, is_closed = true. Convert amount to EUR: if currency_code = 'EUR', amount_eur = amount; otherwise amount_eur = amount / rate from exchange_rates (base_code='EUR', quote_code=currency_code, as_of nearest date on or before closed_at). count_win_rate_pct = closed_won / opportunities_closed * 100. dollar_weighted_win_rate_pct = total_amount_won_eur / (total_amount_won_eur + total_amount_lost_eur) * 100. gap_pct_points = count_win_rate_pct minus dollar_weighted_win_rate_pct. Order by gap_pct_points descending.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Ines calls dry_run
      WITH opp AS (
        SELECT
          o.tenant_id,
          t.name AS merchant,
          o.amount,
          o.currency_code,
          o.closed_at,
          os.is_won,
          os.is_lost
        FROM opportunities o
        JOIN tenants t ON t.id = o.tenant_id
        JOIN pipelines p ON p.id = o.pipeline_id AND p.tenant_id = o.tenant_id
        JOIN opportunity_stages os ON os.id = o.stage_id
        WHERE t.name IN (
          'Ballycorran Industrial Supply','Castell Fasteners','Hanse Industriebedarf',
          'Midland Safety Wear','Polder Packaging'
        )
        AND p.name = 'New business'
        AND o.is_closed = true
      ),
      rated AS (
        SELECT
          opp.merchant,
          opp.is_won,
          opp.is_lost,
          CASE
            WHEN opp.currency_code = 'EUR' THEN opp.amount
            ELSE opp.amount / (
              SELECT er.rate
              FROM exchange_rates er
              WHERE er.base_code = 'EUR'
                AND er.quote_code = opp.currency_code
                AND er.as_of <= opp.closed_at::date
              ORDER BY er.as_of DESC
              LIMIT 1
            )
          END AS amount_eur
        FROM opp
      ),
      agg AS (
        SELECT
          merchant,
          COUNT(*) AS opportunities_closed,
          SUM(CASE WHEN is_won THEN 1 ELSE 0 END) AS won_count,
          SUM(CASE WHEN is_won THEN amount_eur ELSE 0 END) AS total_amount_won_eur,
          SUM(CASE WHEN is_lost THEN amount_eur ELSE 0 END) AS total_amount_lost_eur
        FROM rated
        GROUP BY merchant
      )
      SELECT
        merchant,
        opportunities_closed,
        ROUND(100.0 * won_count / opportunities_closed, 2) AS count_win_rate_pct,
        ROUND(total_amount_won_eur, 2) AS total_amount_won_eur,
        ROUND(total_amount_lost_eur, 2) AS total_amount_lost_eur,
        ROUND(100.0 * total_amount_won_eur / NULLIF(total_amount_won_eur + total_amount_lost_eur, 0), 2) AS dollar_weighted_win_rate_pct,
        ROUND(
          (100.0 * won_count / opportunities_closed)
          - (100.0 * total_amount_won_eur / NULLIF(total_amount_won_eur + total_amount_lost_eur, 0)),
          2
        ) AS gap_pct_points
      FROM agg
      ORDER BY gap_pct_points DESC, merchant ASC
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: merchant, opportunities_closed, count_win_rate_pct, total_amount_won_eur, total_amount_lost_eur, dollar_weighted_win_rate_pct, gap_pct_points
    • InesQuery compiles and returns the expected columns. Submitting now.
    • Ines calls submit
      WITH opp AS (
        SELECT
          o.tenant_id,
          t.name AS merchant,
          o.amount,
          o.currency_code,
          o.closed_at,
          os.is_won,
          os.is_lost
        FROM opportunities o
        JOIN tenants t ON t.id = o.tenant_id
        JOIN pipelines p ON p.id = o.pipeline_id AND p.tenant_id = o.tenant_id
        JOIN opportunity_stages os ON os.id = o.stage_id
        WHERE t.name IN (
          'Ballycorran Industrial Supply','Castell Fasteners','Hanse Industriebedarf',
          'Midland Safety Wear','Polder Packaging'
        )
        AND p.name = 'New business'
        AND o.is_closed = true
      ),
      rated AS (
        SELECT
          opp.merchant,
          opp.is_won,
          opp.is_lost,
          CASE
            WHEN opp.currency_code = 'EUR' THEN opp.amount
            ELSE opp.amount / (
              SELECT er.rate
              FROM exchange_rates er
              WHERE er.base_code = 'EUR'
                AND er.quote_code = opp.currency_code
                AND er.as_of <= opp.closed_at::date
              ORDER BY er.as_of DESC
              LIMIT 1
            )
          END AS amount_eur
        FROM opp
      ),
      agg AS (
        SELECT
          merchant,
          COUNT(*) AS opportunities_closed,
          SUM(CASE WHEN is_won THEN 1 ELSE 0 END) AS won_count,
          SUM(CASE WHEN is_won THEN amount_eur ELSE 0 END) AS total_amount_won_eur,
          SUM(CASE WHEN is_lost THEN amount_eur ELSE 0 END) AS total_amount_lost_eur
        FROM rated
        GROUP BY merchant
      )
      SELECT
        merchant,
        opportunities_closed,
        ROUND(100.0 * won_count / opportunities_closed, 2) AS count_win_rate_pct,
        ROUND(total_amount_won_eur, 2) AS total_amount_won_eur,
        ROUND(total_amount_lost_eur, 2) AS total_amount_lost_eur,
        ROUND(100.0 * total_amount_won_eur / NULLIF(total_amount_won_eur + total_amount_lost_eur, 0), 2) AS dollar_weighted_win_rate_pct,
        ROUND(
          (100.0 * won_count / opportunities_closed)
          - (100.0 * total_amount_won_eur / NULLIF(total_amount_won_eur + total_amount_lost_eur, 0)),
          2
        ) AS gap_pct_points
      FROM agg
      ORDER BY gap_pct_points DESC, merchant ASC
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • InesSubmitted. The report groups the five named wholesale merchants by their own "New business" pipeline's closed opportunities, converts non-EUR amounts using the EUR exchange rate as of (or before) `closed_at`, and computes both count-weighted and dollar-weighted win rates with the gap between them, ordered by the largest gap first — which is where Youssef's "wins small, loses big" pattern would show up most clearly at the merchant level.