DB Olympics

Queries · q390 · Director of Fulfilment Operations · week 2, night 1

Who logs 'Missing - suspected theft' stock adjustments: concentrated among specific staff?

“Is the 'Missing - suspected theft' reason spread roughly proportionally across everyone who logs stock adjustments, or does it concentrate in a handful of specific staff members — and do their theft entries cluster at a particular time of day (suggesting end-of-shift batch logging rather than individually investigated incidents)?”

Manager · asked the questionTomasz NowakDirector of Fulfilment Operations. Ran warehouses for a large retailer before joining.
Analyst · planned the reportFatima ZahraDefinition-minded: checks what each field really records before relying on it.
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 Director of Fulfilment Operations's desk

    Week 1; shared with Tobias Lindqvist, Chief Financial Officer

    Auditors and merchants question inventory accuracy. How often do counts, adjustments and reservations disagree with recorded stock, where, and why?

  2. Tomasz Nowak writes to Fatima Zahra

    The manager, at the start of week 2

    Subject: Re: Inventory accuracy memo — good start, a few things need tightening before I take this to Tobias/auditors

    Fatima,

    This is a strong first cut — the two-mechanism split and the "reservations are fine" finding are exactly what I needed, and I can rule out overselling as a risk now. A few things surprised me and I want them firmed up before we put numbers in front of auditors:

    1. The 24% rise in monthly shrinkage worries me more than the headline total. Is that a steady climb all year, or did it step up around a specific point (new site opening, staffing change, holiday peak)? I want to know if we're looking at a slow-burn trend or something that started at a moment we can point to.
    2. The loss-reason split being almost perfectly even across three categories makes me suspicious, not reassured. Before we size any provision off that split, I want to know whether that's a genuine pattern or an artifact of how staff pick a reason from a dropdown (e.g., defaulting or cycling through options). Can you sanity-check whether specific people, shifts, or sites are behind the "theft-suspected" tag specifically — that's the one that would actually worry legal and the auditors.
    3. On the December 31st batch — I need to understand the "why," not just the "who." If this is confirmed as a real annual B2B stocktake, fine. But I want to understand why a positive correction of that size existed in the first place — were those units sitting miscounted all year, or is this a sign that we simply don't track B2B/bulk inventory day-to-day the way we do pick locations? That has different implications for the auditors than just "we run quarterly/annual stocktakes."
    4. On the apparel/footwear concentration, I want specifics, not just category names. Is this spread evenly across our apparel merchants and sites, or does it cluster around particular sites, shifts, or size runs? If it's a handful of locations, that's a training/process fix I can act on this quarter; if it's spread everywhere, that's a bigger systemic question about how we handle high-variant SKUs generally.

    If you can only run two this week, make it #1 and #2 — those directly affect what Tobias and I say about trend direction and provisioning, which is what the auditors will press hardest on.

    Thanks — good work.

    Tomasz

  3. Fatima Zahra plans the night

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

    Tomasz was explicit: if I can only get to two this week, do #1 (trend shape) and #2 (theft-tag concentration), since those are what he and Tobias will be pressed on. I'm spending all three slots tonight on those two, since each deserves more than a single pass.

    For #1: my existing monthly view (764→946 events/month) shows a fairly flat Jan-Sep and then a clear step up in Oct (865→941, +8.8%) that holds through Nov-Dec. Monthly resolution could be smoothing over or mis-locating the actual inflection point, and it conflates two possible causes: a genuine worsening rate, versus simply more warehouses coming online and doing cycle counts (warehouses_affected already grew 85→111 across the year in my night-1 report). So tonight's trend report uses half-month buckets (fine enough to spot a step, coarse enough to stay well under the row cap) and adds a per-active-location rate alongside raw counts, plus a count of newly-opened warehouses per period, so I can tell scale growth apart from a true rate increase and point to when it started.

    For #2: Tomasz's suspicion is that "theft-suspected" being almost exactly the same size as "damage" and "expired" (1,781/1,755/1,717 events) smells like a dropdown default or habit rather than real incident variety. The sharpest test is whether the tag concentrates in a handful of specific staff or sites rather than being spread proportionally across everyone who logs adjustments. I'm running two cuts — by the staff member who logged the adjustment (adjusted_by_user_id), with a bonus column on what time of day their theft-tagged entries get logged (to catch an end-of-shift batch-logging pattern), and by warehouse/site — since Tomasz asked about both people and sites, and these are two genuinely different grains, not a bundle of the same question.

    #3 (why the Dec-31 B2B batch exists) and #4 (apparel concentration by site/shift) are real work but lower priority per Tomasz's note — I'll pick those up on a later night this week.

    Who logs 'Missing - suspected theft' stock adjustments: concentrated among specific staff?
    Question
    Is the 'Missing - suspected theft' reason spread roughly proportionally across everyone who logs stock adjustments, or does it concentrate in a handful of specific staff members — and do their theft entries cluster at a particular time of day (suggesting end-of-shift batch logging rather than individually investigated incidents)?
    One row per
    staff user (adjusted_by_user_id) with at least one stock_adjustments row in 2025
    Columns
    • staff_name (users.full_name; 'Unknown/unassigned' if adjusted_by_user_id is null)
    • total_adjustments_logged
    • theft_tagged_count (reason = 'Missing - suspected theft')
    • pct_theft_of_own_adjustments (theft_tagged_count / total_adjustments_logged * 100, rounded 1dp)
    • damage_tagged_count (reason = 'Damaged in warehouse')
    • expired_tagged_count (reason = 'Expired')
    • cycle_count_variance_count (reason = 'Cycle count variance')
    • avg_hour_of_day_for_theft_entries (average hour 0-23 extracted from created_at, computed only over this user's theft-tagged rows; null if they have none, rounded 1dp)
  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)
    SELECT
      COALESCE(u.full_name, 'Unknown/unassigned') AS staff_name,
      COUNT(*) AS total_adjustments_logged,
      COUNT(*) FILTER (WHERE sa.reason = 'Missing - suspected theft') AS theft_tagged_count,
      ROUND(100.0 * COUNT(*) FILTER (WHERE sa.reason = 'Missing - suspected theft') / COUNT(*), 1) AS pct_theft_of_own_adjustments,
      COUNT(*) FILTER (WHERE sa.reason = 'Damaged in warehouse') AS damage_tagged_count,
      COUNT(*) FILTER (WHERE sa.reason = 'Expired') AS expired_tagged_count,
      COUNT(*) FILTER (WHERE sa.reason = 'Cycle count variance') AS cycle_count_variance_count,
      ROUND(AVG(EXTRACT(HOUR FROM sa.created_at)) FILTER (WHERE sa.reason = 'Missing - suspected theft'), 1) AS avg_hour_of_day_for_theft_entries,
      COUNT(*) OVER () AS total_staff_with_adjustments_2025
    FROM stock_adjustments sa
    LEFT JOIN users u ON u.id = sa.adjusted_by_user_id
    WHERE sa.created_at >= DATE '2025-01-01' AND sa.created_at < DATE '2026-01-01'
    GROUP BY sa.adjusted_by_user_id, u.full_name
    ORDER BY theft_tagged_count DESC, total_adjustments_logged DESC, staff_name ASC
    LIMIT 50

    “Query compiles cleanly with 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. Summary of what the report does: - Grain: one row per staff member (adjusted_by_user_id), including an "Unknown/unassigned" bucket for null values. - Scope: all stock_adjustments rows with created_at in 2025 (half-open bound), across all tenants, since the question is about staff concentration generally rather than one merchant. - Metrics: total adjustments, theft-tagged count and its share of that staffer's own adjustments, counts for the other three named reasons, and the average hour-of-day (0–23) of that staffer's theft entries — which is the piece that tests the "end-of-shift batch logging" hypothesis. - Extra column: total_staff_with_adjustments_2025, a window count computed over the full grouped result (before the 50-row cap), so the analyst knows the true population size even though the printed page is capped at the top 50 by theft count. - Ordering: theft count desc, then total adjustments desc, then name asc, satisfying the requested tie-breaks and deterministic ordering.”

  5. The report comes back overnight

    As Fatima Zahra saw it, from the small dataset the agents worked on

    staff_name         | total_adjustments_logged | theft_tagged_count | pct_theft_of_own_adjustments | damage_tagged_count | expired_tagged_count | cycle_count_variance_count | avg_hour_of_day_for_theft_entries | total_staff_with_adjustments_2025
    -------------------+--------------------------+--------------------+------------------------------+---------------------+----------------------+----------------------------+-----------------------------------+----------------------------------
    Unknown/unassigned |                     3570 |                640 |                         17.9 |                 609 |                  605 |                       1716 |                              11.4 |                                35
    Christopher Evans  |                     1506 |                262 |                         17.4 |                 264 |                  256 |                        724 |                              11.5 |                                35
    Omar Taylor        |                     1058 |                186 |                         17.6 |                 187 |                  178 |                        507 |                              11.2 |                                35
    Noor de Jong       |                      622 |                115 |                         18.5 |                 102 |                  100 |                        305 |                              11.5 |                                35
    Harry Pappas       |                      253 |                 57 |                         22.5 |                  41 |                   39 |                        116 |                              12.0 |                                35
    Olivia Larsson     |                      304 |                 53 |                         17.4 |                  59 |                   51 |                        141 |                              11.5 |                                35
    Antoine Blanc      |                      286 |                 52 |                         18.2 |                  51 |                   45 |                        138 |                              11.8 |                                35
    Niklas Mayer       |                      244 |                 42 |                         17.2 |                  45 |                   31 |                        126 |                              11.7 |                                35
    Emma Hall          |                      173 |                 40 |                         23.1 |                  22 |                   25 |                         86 |                              11.3 |                                35
    Mikkel Sørensen    |                      216 |                 39 |                         18.1 |                  40 |                   38 |                         99 |                              11.5 |                                35
    Sarah Peters       |                      219 |                 37 |                         16.9 |                  36 |                   46 |                        100 |                              12.0 |                                35
    Manon Dubois       |                      209 |                 35 |                         16.7 |                  28 |                   37 |                        109 |                              11.8 |                                35
    Taylor Young       |                      262 |                 33 |                         12.6 |                  48 |                   48 |                        133 |                              10.4 |                                35
    Michael Weiß       |                      145 |                 23 |                         15.9 |                  27 |                   21 |                         74 |                              12.1 |                                35
    Mikkel Olsen       |                      135 |                 23 |                         17.0 |                  28 |                   27 |                         57 |                              10.7 |                                35
    Stefan Vos         |                      134 |                 20 |                         14.9 |                  22 |                   20 |                         72 |                              11.1 |                                35
    Paula Gruber       |                       71 |                 15 |                         21.1 |                  11 |                   15 |                         30 |                              12.0 |                                35
    Milan de Vries     |                       76 |                 13 |                         17.1 |                  12 |                   11 |                         40 |                              10.9 |                                35
    Mia Mayer          |                       76 |                 12 |                         15.8 |                  12 |                    9 |                         43 |                              10.7 |                                35
    Ava Davis          |                       57 |                 10 |                         17.5 |                  10 |                   12 |                         25 |                              10.5 |                                35
    Johanna Kaiser     |                       54 |                  9 |                         16.7 |                   6 |                    8 |                         31 |                              11.9 |                                35
    Ethan Lewis        |                       48 |                  8 |                         16.7 |                   8 |                    5 |                         27 |                              11.8 |                                35
    William Jensen     |                       45 |                  8 |                         17.8 |                   6 |                    8 |                         23 |                              12.5 |                                35
    Youssef Kok        |                       39 |                  8 |                         20.5 |                  10 |                    5 |                         16 |                              10.1 |                                35
    Knut Jensen        |                       48 |                  7 |                         14.6 |                   9 |                   10 |                         22 |                              11.7 |                                35
    Jack King          |                       78 |                  6 |                          7.7 |                  14 |                   16 |                         42 |                              12.7 |                                35
    Laura Lopez        |                       80 |                  5 |                          6.3 |                  17 |                   15 |                         43 |                              12.6 |                                35
    Mason Johnson      |                       23 |                  5 |                         21.7 |                   2 |                    2 |                         14 |                              10.4 |                                35
    Jonas Weiß         |                       28 |                  4 |                         14.3 |                   2 |                    6 |                         16 |                              14.0 |                                35
    Greta Weiß         |                       27 |                  4 |                         14.8 |                   7 |                    7 |                          9 |                              12.0 |                                35
    Grace Jackson      |                       31 |                  3 |                          9.7 |                   9 |                    7 |                         12 |                              14.3 |                                35
    Maja Muller        |                       23 |                  3 |                         13.0 |                   6 |                    4 |                         10 |                              11.7 |                                35
    Aoife Ryan         |                       29 |                  2 |                          6.9 |                   4 |                    4 |                         19 |                              12.5 |                                35
    George Fernandes   |                       24 |                  2 |                          8.3 |                   1 |                    6 |                         15 |                              12.5 |                                35
    Jack Morgan        |                        1 |                  0 |                          0.0 |                   0 |                    0 |                          0 |                              NULL |                                35
    (35 rows)
  6. It becomes benchmark query q390

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

    • spellingfetch first
    • determinismtie-breaker: staff names are not unique; adjusted_by_user_id is the group key
    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision
    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision
    • determinismthe standard leaves the place of NULLs in a sort to the implementation: NULLS LAST ascending, NULLS FIRST descending, as in the reference results (4×)
    • placeholderfetch first
    The benchmark text, against the submitted SQL
    SELECT  COALESCE(u.full_name, 'Unknown/unassigned') AS staff_name,  COUNT(*) AS total_adjustments_logged,  COUNT(*) FILTER (WHERE sa.reason = 'Missing - suspected theft') AS theft_tagged_count,  ROUND(100.0 * COUNT(*) FILTER (WHERE sa.reason = 'Missing - suspected theft') / COUNT(*), 1) AS pct_theft_of_own_adjustments,  100.0 * COUNT(*) FILTER (WHERE sa.reason = 'Missing - suspected theft') / COUNT(*) AS pct_theft_of_own_adjustments,  COUNT(*) FILTER (WHERE sa.reason = 'Damaged in warehouse') AS damage_tagged_count,  COUNT(*) FILTER (WHERE sa.reason = 'Expired') AS expired_tagged_count,  COUNT(*) FILTER (WHERE sa.reason = 'Cycle count variance') AS cycle_count_variance_count,  ROUND(AVG(EXTRACT(HOUR FROM sa.created_at)) FILTER (WHERE sa.reason = 'Missing - suspected theft'), 1) AS avg_hour_of_day_for_theft_entries,  AVG(EXTRACT(HOUR FROM sa.created_at)) FILTER (WHERE sa.reason = 'Missing - suspected theft') AS avg_hour_of_day_for_theft_entries,  COUNT(*) OVER () AS total_staff_with_adjustments_2025FROM stock_adjustments saLEFT JOIN users u ON u.id = sa.adjusted_by_user_idWHERE sa.created_at >= DATE '2025-01-01' AND sa.created_at < DATE '2026-01-01'GROUP BY sa.adjusted_by_user_id, u.full_nameORDER BY theft_tagged_count DESC, total_adjustments_logged DESC, staff_name ASCLIMIT 50ORDER BY theft_tagged_count DESC NULLS FIRST, total_adjustments_logged DESC NULLS FIRST, staff_name ASC NULLS LAST, sa.adjusted_by_user_id ASC NULLS LAST{{fetch_first(50)}}
    SELECT
      COALESCE(u.full_name, 'Unknown/unassigned') AS staff_name,
      COUNT(*) AS total_adjustments_logged,
      COUNT(*) FILTER (WHERE sa.reason = 'Missing - suspected theft') AS theft_tagged_count,
      100.0 * COUNT(*) FILTER (WHERE sa.reason = 'Missing - suspected theft') / COUNT(*) AS pct_theft_of_own_adjustments,
      COUNT(*) FILTER (WHERE sa.reason = 'Damaged in warehouse') AS damage_tagged_count,
      COUNT(*) FILTER (WHERE sa.reason = 'Expired') AS expired_tagged_count,
      COUNT(*) FILTER (WHERE sa.reason = 'Cycle count variance') AS cycle_count_variance_count,
      AVG(EXTRACT(HOUR FROM sa.created_at)) FILTER (WHERE sa.reason = 'Missing - suspected theft') AS avg_hour_of_day_for_theft_entries,
      COUNT(*) OVER () AS total_staff_with_adjustments_2025
    FROM stock_adjustments sa
    LEFT JOIN users u ON u.id = sa.adjusted_by_user_id
    WHERE sa.created_at >= DATE '2025-01-01' AND sa.created_at < DATE '2026-01-01'
    GROUP BY sa.adjusted_by_user_id, u.full_name
    ORDER BY theft_tagged_count DESC NULLS FIRST, total_adjustments_logged DESC NULLS FIRST, staff_name ASC NULLS LAST, sa.adjusted_by_user_id ASC NULLS LAST
    FETCH FIRST 50 ROWS ONLY
    10 ms100 ms1 sHyper37.8 msDuckDB37.4 msGizmoSQL49.6 msclickhouse_brew59.8 msFirebolt58.9 msDataFusion257 msClickHouse47.0 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

    Ines's whole session, 7 turns
    • The request, as Ines received it
      Report request from Fatima Zahra (analyst for the Director of Fulfilment Operations):
      
      Title: Who logs 'Missing - suspected theft' stock adjustments: concentrated among specific staff?
      Question: Is the 'Missing - suspected theft' reason spread roughly proportionally across everyone who logs stock adjustments, or does it concentrate in a handful of specific staff members — and do their theft entries cluster at a particular time of day (suggesting end-of-shift batch logging rather than individually investigated incidents)?
      Grain: staff user (adjusted_by_user_id) with at least one stock_adjustments row in 2025
      Columns:
      - staff_name (users.full_name; 'Unknown/unassigned' if adjusted_by_user_id is null)
      - total_adjustments_logged
      - theft_tagged_count (reason = 'Missing - suspected theft')
      - pct_theft_of_own_adjustments (theft_tagged_count / total_adjustments_logged * 100, rounded 1dp)
      - damage_tagged_count (reason = 'Damaged in warehouse')
      - expired_tagged_count (reason = 'Expired')
      - cycle_count_variance_count (reason = 'Cycle count variance')
      - avg_hour_of_day_for_theft_entries (average hour 0-23 extracted from created_at, computed only over this user's theft-tagged rows; null if they have none, rounded 1dp)
      Details: Source: stock_adjustments, created_at between 2025-01-01 and 2025-12-31 (half-open bound). Left join users on adjusted_by_user_id for staff_name. Group by adjusted_by_user_id (treat null as its own 'Unknown/unassigned' group). Order by theft_tagged_count descending, tie-break by total_adjustments_logged descending, then staff_name ascending. Cap at 50 rows (top 50 by theft_tagged_count) and note the total distinct staff count with >=1 adjustment in 2025 even if it exceeds 50.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Ines calls dry_run
      SELECT
        COALESCE(u.full_name, 'Unknown/unassigned') AS staff_name,
        COUNT(*) AS total_adjustments_logged,
        COUNT(*) FILTER (WHERE sa.reason = 'Missing - suspected theft') AS theft_tagged_count,
        ROUND(100.0 * COUNT(*) FILTER (WHERE sa.reason = 'Missing - suspected theft') / COUNT(*), 1) AS pct_theft_of_own_adjustments,
        COUNT(*) FILTER (WHERE sa.reason = 'Damaged in warehouse') AS damage_tagged_count,
        COUNT(*) FILTER (WHERE sa.reason = 'Expired') AS expired_tagged_count,
        COUNT(*) FILTER (WHERE sa.reason = 'Cycle count variance') AS cycle_count_variance_count,
        ROUND(AVG(EXTRACT(HOUR FROM sa.created_at)) FILTER (WHERE sa.reason = 'Missing - suspected theft'), 1) AS avg_hour_of_day_for_theft_entries,
        COUNT(*) OVER () AS total_staff_with_adjustments_2025
      FROM stock_adjustments sa
      LEFT JOIN users u ON u.id = sa.adjusted_by_user_id
      WHERE sa.created_at >= DATE '2025-01-01' AND sa.created_at < DATE '2026-01-01'
      GROUP BY sa.adjusted_by_user_id, u.full_name
      ORDER BY theft_tagged_count DESC, total_adjustments_logged DESC, staff_name ASC
      LIMIT 50
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: staff_name, total_adjustments_logged, theft_tagged_count, pct_theft_of_own_adjustments, damage_tagged_count, expired_tagged_count, cycle_count_variance_count, avg_hour_of_day_for_theft_entries, total_staff_with_adjustments_2025
    • InesQuery compiles cleanly with the expected columns. Submitting now.
    • Ines calls submit
      SELECT
        COALESCE(u.full_name, 'Unknown/unassigned') AS staff_name,
        COUNT(*) AS total_adjustments_logged,
        COUNT(*) FILTER (WHERE sa.reason = 'Missing - suspected theft') AS theft_tagged_count,
        ROUND(100.0 * COUNT(*) FILTER (WHERE sa.reason = 'Missing - suspected theft') / COUNT(*), 1) AS pct_theft_of_own_adjustments,
        COUNT(*) FILTER (WHERE sa.reason = 'Damaged in warehouse') AS damage_tagged_count,
        COUNT(*) FILTER (WHERE sa.reason = 'Expired') AS expired_tagged_count,
        COUNT(*) FILTER (WHERE sa.reason = 'Cycle count variance') AS cycle_count_variance_count,
        ROUND(AVG(EXTRACT(HOUR FROM sa.created_at)) FILTER (WHERE sa.reason = 'Missing - suspected theft'), 1) AS avg_hour_of_day_for_theft_entries,
        COUNT(*) OVER () AS total_staff_with_adjustments_2025
      FROM stock_adjustments sa
      LEFT JOIN users u ON u.id = sa.adjusted_by_user_id
      WHERE sa.created_at >= DATE '2025-01-01' AND sa.created_at < DATE '2026-01-01'
      GROUP BY sa.adjusted_by_user_id, u.full_name
      ORDER BY theft_tagged_count DESC, total_adjustments_logged DESC, staff_name ASC
      LIMIT 50
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • InesSubmitted. Summary of what the report does: - **Grain:** one row per staff member (`adjusted_by_user_id`), including an "Unknown/unassigned" bucket for null values. - **Scope:** all `stock_adjustments` rows with `created_at` in 2025 (half-open bound), across all tenants, since the question is about staff concentration generally rather than one merchant. - **Metrics:** total adjustments, theft-tagged count and its share of that staffer's own adjustments, counts for the other three named reasons, and the average hour-of-day (0–23) of that staffer's theft entries — which is the piece that tests the "end-of-shift batch logging" hypothesis. - **Extra column:** `total_staff_with_adjustments_2025`, a window count computed over the full grouped result (before the 50-row cap), so the analyst knows the true population size even though the printed page is capped at the top 50 by theft count. - **Ordering:** theft count desc, then total adjustments desc, then name asc, satisfying the requested tie-breaks and deterministic ordering.