DB Olympics

Queries · q141 · Head of Merchandising · week 1, night 2

December Rating Dip by Vertical: 2024 vs 2025

“Which merchant vertical drove the platform-wide rating dip and spike in negative reviews seen in December 2025?”

Manager · asked the questionFreya LindgrenHead of Merchandising. Came from a fashion marketplace's catalog team. Responsible for helping merchants present their products well.
Analyst · planned the reportZara AhmedExploratory: looks at the raw detail of an unfamiliar area before aggregating it.
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. Freya Lindgren writes to Zara Ahmed

    The manager, at the start of week 1

    Subject: My first-week priorities — catalog questions

    Hi Zara,

    Great to have you helping me get oriented this week. I know slots are tight, so here's what I most want to understand first. Pick the ones you think will give us the clearest picture given the report budget — happy to defer the rest to next week.

    1. How healthy is our catalog overall, and where is it thin? I'd like a sense of how many products and variants each merchant is actually running, which ones feel well-built out (good imagery, descriptions, variants) versus sparse, and whether that varies a lot across merchants or verticals. I need this to know where to focus my attention first.
    2. Does catalog quality actually move sales? I'm curious whether products with richer content — more images, more reviews, better ratings — sell noticeably better than ones without. I want to build the case (to myself and to merchants) for why investing in catalog presentation is worth it.
    3. What do customers think of the products, and where are the trouble spots? I'd like to see how reviews and ratings are trending, which products or categories are getting poor feedback, and whether there are products with surprisingly few or no reviews despite decent sales. This tells me where to nudge merchants on quality or listing improvements.
    4. Are we using collections and bundles well? I want to know how much merchants are leaning on curated collections and bundles versus just flat product listings, and whether those grouped presentations correlate with better performance. This will shape whether I push merchandising tools harder in my conversations with merchants.
    5. How are our top merchants' catalogs structured compared to the long tail? Since a handful of merchants drive most of the volume, I'd like to understand if their catalogs look meaningfully different (size, categorization, media richness) from smaller merchants — it'll help me figure out what "good" looks like and what advice to bring to the smaller ones.

    Let me know what's realistic to cover by Friday, and flag if any of these need to be split into smaller questions on your end.

    Thanks, Freya

  2. Zara Ahmed plans the night

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

    Night 1 findings to react to: (1) In the merchant profile, image-presence (97-100%) and variant richness are almost entirely explained by vertical (apparel ~12-14 variants/product vs B2B ~1-2), not merchant skill — so 'catalog health' comparisons need to control for vertical. A couple of outliers stood out for later: Bytefield's avg_rating of 1.50 and Circuit Harbor's thin 18 orders against 93 products, both worth a Q3 drill-down later. (2) The image-count-bucket report is NOT monotonic — bucket '1 image' (265 EUR/product) beats '2-3' and '4-6' (183, 192), only catching back up at '7+' (521). That's suspicious and likely confounded by vertical mix (B2B products tend to have 1 plain image but high per-order value; apparel has many images but lower per-unit revenue). I want to re-run the richness-vs-sales comparison inside a single vertical to remove that confound before telling Freya 'more images = more sales'. (3) The monthly review trend shows review counts climbing through the series with sharp December spikes each year, but December 2025 stands out with the lowest avg_rating (3.71) and highest pct_1_2_star (25.8%) in the whole 36-month series — a real anomaly worth root-causing. Also, since order volume is growing over the same period, I can't yet tell if rising review counts reflect genuine engagement growth or just mirror order growth — need review-per-order to normalize that. Tonight's three requests: control for vertical on richness-vs-sales, normalize review volume by order volume, and localize the December 2025 rating dip by vertical.

    December Rating Dip by Vertical: 2024 vs 2025
    Question
    Which merchant vertical drove the platform-wide rating dip and spike in negative reviews seen in December 2025?
    One row per
    tenant industry (vertical)
    Columns
    • industry
    • dec2024_review_count
    • dec2024_avg_rating
    • dec2025_review_count
    • dec2025_avg_rating
    • rating_change_pts
    • dec2025_pct_1_2_star
  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)
    SELECT
      t.industry,
      COUNT(*) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01') AS dec2024_review_count,
      ROUND(AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01')::numeric, 2) AS dec2024_avg_rating,
      COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01') AS dec2025_review_count,
      ROUND(AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')::numeric, 2) AS dec2025_avg_rating,
      ROUND(
        (AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')
         - AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01'))::numeric
      , 2) AS rating_change_pts,
      ROUND(
        100.0 * COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01' AND pr.rating <= 2)
        / NULLIF(COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01'), 0)
      , 1) AS dec2025_pct_1_2_star
    FROM product_reviews pr
    JOIN products p ON p.id = pr.product_id AND p.tenant_id = pr.tenant_id
    JOIN tenants t ON t.id = pr.tenant_id
    WHERE pr.is_approved = true
      AND (
        (pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01')
        OR (pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')
      )
    GROUP BY t.industry
    ORDER BY rating_change_pts ASC NULLS LAST, t.industry ASC

    “Query compiles and produces the expected columns. Submitting.”

    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 approved product reviews by tenants.industry, comparing full-calendar-month December 2024 vs December 2025 counts and average ratings, with the rating delta and the Dec-2025 1-2 star share, ordered worst-decline first.”

  4. The report comes back overnight

    As Zara Ahmed saw it, from the small dataset the agents worked on

    industry                  | dec2024_review_count | dec2024_avg_rating | dec2025_review_count | dec2025_avg_rating | rating_change_pts | dec2025_pct_1_2_star
    --------------------------+----------------------+--------------------+----------------------+--------------------+-------------------+---------------------
    Jewelry & Accessories     |                    2 |               5.00 |                    1 |               3.00 |             -2.00 |                  0.0
    Office Supplies (B2B)     |                    5 |               5.00 |                    9 |               3.67 |             -1.33 |                 33.3
    Pet Supplies              |                   10 |               4.50 |                   18 |               3.61 |             -0.89 |                 33.3
    Books & Stationery        |                    5 |               4.40 |                    4 |               3.75 |             -0.65 |                 25.0
    Consumer Electronics      |                   13 |               4.15 |                   17 |               3.65 |             -0.51 |                 29.4
    Home & Living             |                   42 |               3.86 |                   59 |               3.36 |             -0.50 |                 35.6
    Apparel & Footwear        |                  207 |               3.78 |                  219 |               3.68 |             -0.11 |                 25.1
    Beauty                    |                   24 |               3.83 |                   43 |               3.72 |             -0.11 |                 23.3
    Food & Coffee             |                   65 |               4.29 |                   61 |               4.20 |             -0.10 |                 16.4
    Sports & Outdoor          |                   15 |               3.80 |                   18 |               3.78 |             -0.02 |                 27.8
    Industrial Supplies (B2B) |                    4 |               4.00 |                    5 |               4.00 |              0.00 |                 20.0
    (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 q141

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

    • spellingcast (3×)
    • 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: 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)))
    • 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
    The benchmark text, against the submitted SQL
    SELECT  t.industry,  t.industry AS industry,  COUNT(*) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01') AS dec2024_review_count,  ROUND(AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01')::numeric, 2) AS dec2024_avg_rating,  AVG(CAST(pr.rating AS NUMERIC(18,6))) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01') AS dec2024_avg_rating,  COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01') AS dec2025_review_count,  ROUND(AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')::numeric, 2) AS dec2025_avg_rating,  ROUND(    (AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')     - AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01'))::numeric  , 2) AS rating_change_pts,  ROUND(    100.0 * COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01' AND pr.rating <= 2)    / NULLIF(COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01'), 0)  , 1) AS dec2025_pct_1_2_star  AVG(CAST(pr.rating AS NUMERIC(18,6))) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01') AS dec2025_avg_rating,  (AVG(CAST(pr.rating AS NUMERIC(18,6))) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')- AVG(CAST(pr.rating AS NUMERIC(18,6))) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01')) AS rating_change_pts,  100.0 * COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01' AND pr.rating <= 2)  / NULLIF(COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01'), 0) AS dec2025_pct_1_2_starFROM product_reviews prJOIN products p ON p.id = pr.product_id AND p.tenant_id = pr.tenant_idJOIN tenants t ON t.id = pr.tenant_idWHERE pr.is_approved = true  AND (    (pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01')    OR (pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')  )GROUP BY t.industryORDER BY rating_change_pts ASC NULLS LAST, t.industry ASCORDER BY rating_change_pts ASC NULLS LAST, t.industry ASC NULLS LAST
    SELECT
      t.industry AS industry,
      COUNT(*) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01') AS dec2024_review_count,
      AVG(CAST(pr.rating AS NUMERIC(18,6))) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01') AS dec2024_avg_rating,
      COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01') AS dec2025_review_count,
      AVG(CAST(pr.rating AS NUMERIC(18,6))) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01') AS dec2025_avg_rating,
      (AVG(CAST(pr.rating AS NUMERIC(18,6))) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')
    - AVG(CAST(pr.rating AS NUMERIC(18,6))) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01')) AS rating_change_pts,
      100.0 * COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01' AND pr.rating <= 2)
      / NULLIF(COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01'), 0) AS dec2025_pct_1_2_star
    FROM product_reviews pr
    JOIN products p ON p.id = pr.product_id AND p.tenant_id = pr.tenant_id
    JOIN tenants t ON t.id = pr.tenant_id
    WHERE pr.is_approved = true
      AND (
        (pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01')
        OR (pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')
      )
    GROUP BY t.industry
    ORDER BY rating_change_pts ASC NULLS LAST, t.industry ASC NULLS LAST
    10 ms100 ms1 sHyper13.8 msDuckDB40.4 msGizmoSQL112 msclickhouse_brew70.1 msFirebolt39.7 msDataFusion81.4 msClickHouse66.3 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

    Omar's whole session, 7 turns
    • The request, as Omar received it
      Report request from Zara Ahmed (analyst for the Head of Merchandising):
      
      Title: December Rating Dip by Vertical: 2024 vs 2025
      Question: Which merchant vertical drove the platform-wide rating dip and spike in negative reviews seen in December 2025?
      Grain: one row per tenant industry (vertical)
      Columns:
      - industry
      - dec2024_review_count
      - dec2024_avg_rating
      - dec2025_review_count
      - dec2025_avg_rating
      - rating_change_pts
      - dec2025_pct_1_2_star
      Details: Join product_reviews (is_approved = true) to products to tenants for industry. Compare the full calendar months of December 2024 (2024-12-01 to 2024-12-31) and December 2025 (2025-12-01 to 2025-12-31) based on product_reviews.created_at. dec2024_review_count / dec2024_avg_rating and dec2025_review_count / dec2025_avg_rating are the review count and mean rating for that industry in each month. rating_change_pts = dec2025_avg_rating minus dec2024_avg_rating, rounded to 2 decimals (negative means it got worse). dec2025_pct_1_2_star = share of that industry's Dec 2025 approved reviews rated 1 or 2, rounded to 1 decimal. Order by rating_change_pts ascending so the vertical with the biggest decline is first.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Omar calls dry_run
      SELECT
        t.industry,
        COUNT(*) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01') AS dec2024_review_count,
        ROUND(AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01')::numeric, 2) AS dec2024_avg_rating,
        COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01') AS dec2025_review_count,
        ROUND(AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')::numeric, 2) AS dec2025_avg_rating,
        ROUND(
          (AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')
           - AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01'))::numeric
        , 2) AS rating_change_pts,
        ROUND(
          100.0 * COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01' AND pr.rating <= 2)
          / NULLIF(COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01'), 0)
        , 1) AS dec2025_pct_1_2_star
      FROM product_reviews pr
      JOIN products p ON p.id = pr.product_id AND p.tenant_id = pr.tenant_id
      JOIN tenants t ON t.id = pr.tenant_id
      WHERE pr.is_approved = true
        AND (
          (pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01')
          OR (pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')
        )
      GROUP BY t.industry
      ORDER BY rating_change_pts ASC NULLS LAST, t.industry ASC
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: industry, dec2024_review_count, dec2024_avg_rating, dec2025_review_count, dec2025_avg_rating, rating_change_pts, dec2025_pct_1_2_star
    • OmarQuery compiles and produces the expected columns. Submitting.
    • Omar calls submit
      SELECT
        t.industry,
        COUNT(*) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01') AS dec2024_review_count,
        ROUND(AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01')::numeric, 2) AS dec2024_avg_rating,
        COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01') AS dec2025_review_count,
        ROUND(AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')::numeric, 2) AS dec2025_avg_rating,
        ROUND(
          (AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')
           - AVG(pr.rating) FILTER (WHERE pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01'))::numeric
        , 2) AS rating_change_pts,
        ROUND(
          100.0 * COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01' AND pr.rating <= 2)
          / NULLIF(COUNT(*) FILTER (WHERE pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01'), 0)
        , 1) AS dec2025_pct_1_2_star
      FROM product_reviews pr
      JOIN products p ON p.id = pr.product_id AND p.tenant_id = pr.tenant_id
      JOIN tenants t ON t.id = pr.tenant_id
      WHERE pr.is_approved = true
        AND (
          (pr.created_at >= DATE '2024-12-01' AND pr.created_at < DATE '2025-01-01')
          OR (pr.created_at >= DATE '2025-12-01' AND pr.created_at < DATE '2026-01-01')
        )
      GROUP BY t.industry
      ORDER BY rating_change_pts ASC NULLS LAST, t.industry ASC
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • OmarSubmitted. The report groups approved product reviews by `tenants.industry`, comparing full-calendar-month December 2024 vs December 2025 counts and average ratings, with the rating delta and the Dec-2025 1-2 star share, ordered worst-decline first.