skip to content

What breaks when you wrap extra columns in MIN() just to satisfy the GROUP BY rule?

level: seniorimportance: should knowfreq 45%

answer

  1. The query compiles but the report is wrong
  2. Each aggregate scans the group by itself
  3. Nothing ties two aggregates to one row
  4. A row that exists in no table
  5. ANY_VALUE is honest, not consistent

basics

~20 s

Each aggregate is evaluated independently over the whole group, so MIN(order_date) and MIN(order_total) can come from different rows. The output row is a composite that may match no real row, and no error is raised.

solid answer

~50 s

Wrapping a column in `MIN`, `MAX` or MySQL's `ANY_VALUE` makes the query legal, but it does not make it correct. **Every aggregate is computed independently over the group**, so `MIN(order_date)` and `MIN(order_total)` are chosen from potentially different rows; the result is a stitched-together row that may never have existed in the table. The failure is silent — there is no error, and on small test data the values often happen to coincide. Use an aggregate only when that aggregate is genuinely what you mean ("the earliest date", "the largest total"). When you actually want *the row* that owns an extremum, express that: rank the rows and filter, or compute the key in a subquery and join back to fetch the rest of that row. `ANY_VALUE` is at least honest — it declares the value arbitrary — but it is no more row-consistent.

code

sql · 7 lines
sql
-- Wrong: the two MINs are computed independently over the group,
-- so they can come from different orders.
SELECT customer_id,
       MIN(order_date)  AS first_order_date,
       MIN(order_total) AS first_order_total  -- lies
FROM orders
GROUP BY customer_id;

go deeper

for a junior

Remember that an aggregate summarizes the whole group, not one chosen row, so MIN on two columns need not describe the same record.

for a middle

Explain the independence of aggregates with a concrete two-row example, and show the join-back or ranking rewrite that keeps all columns from one row.

for a senior

Treat it as a silent data-correctness bug: it never errors, small fixtures hide it, and misleading aliases carry it through review. Say how you would detect and test for it.

for a principal

Make it a review rule — several aggregates over different columns in one grouped select list must be justified, and reports assembled from mixed rows are a reconciliation risk, not a style nit.

## The shortcut and why it is tempting You write a per-customer summary, hit the every-selected-column error, and the fastest edit that compiles is to wrap the offending columns in an aggregate: ```sql SELECT customer_id, MIN(order_date) AS first_order_date, MIN(order_total) AS smallest_total FROM orders GROUP BY customer_id; ``` The query runs. The column names read as though they describe one order. They do not. ## Aggregates are independent projections of the group An aggregate function maps the *whole group* to one value; it does not select a representative row and read columns from it. `MIN(order_date)` scans every order of the customer and returns the smallest date. `MIN(order_total)` independently scans the same group and returns the smallest total. Nothing ties the two results to the same source row. With two orders for customer 1 — `('2024-01-05', 90)` and `('2024-03-02', 10)` — the output row is `(1, '2024-01-05', 10)`: the date of the first order beside the total of the second. If a column named `first_order_date` sits next to one named `smallest_total`, the composite is arguably fine; if the second is named `first_order_total`, the report is simply wrong. This is sometimes called a *Frankenstein row*: a tuple assembled from parts of several rows that corresponds to no fact in the database. ## Why it survives review Three properties make this defect durable: 1. **It never errors.** The statement is valid SQL; the engine has nothing to complain about. 2. **Small data hides it.** If your fixture has one row per customer, or the extremes coincide, every value agrees and the test passes. 3. **The column alias lies for you.** `MIN(status) AS status` reads like "the status", and reviewers skim past it. The production symptom is a report where the pieces individually look plausible and the combination does not reconcile — a first-order date whose amount does not match any order. ## ANY_VALUE and permissive modes MySQL offers `ANY_VALUE(col)`, which returns an unspecified value from the group and suppresses the `ONLY_FULL_GROUP_BY` check for that expression. It is preferable to `MIN` in one respect only: it *documents* that the value is arbitrary rather than dressing indeterminacy up as a meaningful extremum. It buys no row consistency — two `ANY_VALUE` calls in one select list may still come from different rows. The same caveat applies to engines that quietly permit bare ungrouped columns. A reasonable rule of thumb: use `MIN`/`MAX` when the extremum is the answer you are reporting; use `ANY_VALUE` (or a bare column, where tolerated) only for a column that is genuinely constant within the group — typically one functionally dependent on the grouping key, where any value is *the* value. ## Expressing what you actually meant When the requirement is "the whole row that owns the extremum", say so in the query rather than aggregating column by column. One portable shape computes the key in a grouped subquery and joins back to read the rest of that row: ```sql SELECT o.customer_id, o.order_date, o.order_total FROM orders o JOIN (SELECT customer_id, MIN(order_date) AS first_order_date FROM orders GROUP BY customer_id) f ON f.customer_id = o.customer_id AND f.first_order_date = o.order_date; ``` Now every column of the output comes from the same physical row, because the join re-reads that row. Note the tie caveat: if a customer has two orders on the same earliest date, both appear — decide deliberately whether that is right, and add a tiebreaker if not. Ranking-and-filtering approaches solve the same problem with an explicit ordering and are the usual alternative. ## Reviewing for it When you see several aggregates over *different* columns in one grouped select list, ask a single question: *is each of these values individually the answer, or are they meant to describe one row?* If it is the latter, the query is wrong regardless of what the tests say. Look especially at aliases that drop the aggregate from the name (`MAX(email) AS email`) — that rename is where the intent quietly shifts from "the largest email" to "the customer's email".

  • When is MIN() on an extra column perfectly legitimate?
    When the extremum itself is the answer — "earliest signup date per country", "largest single order per customer" — or when the column is constant within the group, such as a column functionally dependent on the grouping key. The defect appears only when several aggregates are meant to describe one shared row.
  • Is MySQL's ANY_VALUE a safer choice than MIN for silencing the error?
    Safer in intent, not in behaviour. It states plainly that the value is arbitrary instead of disguising it as a meaningful minimum, which helps reviewers. It still gives no guarantee that two ANY_VALUE calls read the same row, so use it only for columns that are constant within the group.
  • What tie hazard does the join-back pattern introduce?
    If two rows share the extremum — two orders on the same earliest date — the join matches both and the group yields two output rows. Decide deliberately: add a deterministic tiebreaker to the join condition, or accept the duplicates. A summary that silently doubles is its own defect.

saying these in an interview costs you the question

  • Assumes all aggregates read the same representative row
  • Renames MAX(email) to email and calls it the value
  • Treats ANY_VALUE as guaranteeing row consistency
  • Says tests pass, so the grouped query must be right
  • Adds aggregates purely to make the parser stop complaining

context