skip to content

Aggregation and Grouping

GROUP BY mechanics and the aggregate functions, how aggregates treat NULLs, HAVING versus WHERE, GROUPING SETS, ROLLUP and CUBE, conditional and string aggregation, and the fan-out you get aggregating over a join. Interviewers ask because a doubled SUM caused by a one-to-many join is the classic reporting bug.

part ofSQLoverview, primer and where to startread it →
on this pageshow

questions

page 2 of 2

Can GROUP BY take an expression instead of a column, and how do you bucket a timestamp by day?

level: middleimportance: should knowfreq 60%

basics

~20 s

Yes — GROUP BY accepts any value expression over the input columns, and groups are keyed on the computed value. Bucketing a timestamp by day means grouping on a truncated form, such as CAST(created_at AS DATE), not on the raw timestamp.

open as a page

Why does SELECT COUNT(*), SUM(amount) FROM orders WHERE region = 'ZZ' return a row when nothing matches?

level: middleimportance: should knowfreq 55%

basics

~20 s

With no GROUP BY, an aggregate query treats the whole filtered input as one implicit group and always returns exactly one row, even if nothing matched. Add GROUP BY and empty input yields zero groups, so zero rows.

open as a page

What does GROUP BY do when the SELECT list has no aggregate, and how does it compare with SELECT DISTINCT?

level: middleimportance: should knowfreq 50%

basics

~20 s

Grouping still happens: rows are partitioned by the key and each group emits its key, so the result is the distinct key combinations — the same row set SELECT DISTINCT would give over those columns. GROUP BY additionally supports aggregates and group-level filtering.

open as a page

How do GROUP BY ROLLUP(a, b, c) and GROUP BY CUBE(a, b, c) differ in the grouping sets they generate?

level: middleimportance: should knowfreq 44%

basics

~20 s

ROLLUP generates the n+1 prefixes of the column list, so four grouping sets for three columns. CUBE generates every subset — 2^n, or eight for three columns — including combinations that skip a leading column.

open as a page

How would you return per-region, per-product and overall totals in one query using GROUPING SETS?

level: middleimportance: should knowfreq 38%

basics

~10 s

Write GROUP BY GROUPING SETS ((region), (product), ()) with SUM in the select list. Each listed set becomes its own aggregation level in one result, and the empty set () yields the grand total.

open as a page

What does a HAVING clause do in a query that has no GROUP BY?

level: middleimportance: should knowfreq 35%

basics

~10 s

Without GROUP BY, every row surviving WHERE forms one implicit group, and HAVING is a single test over that group's aggregates. The query returns either the one aggregate row or no rows at all.

open as a page

In a per-department salary query, how does WHERE salary > 50000 differ from HAVING AVG(salary) > 50000?

level: middleimportance: should knowfreq 62%

basics

~20 s

WHERE drops low-paid employees before grouping, so a department appears if it has any high earner and its reported average covers only those employees. HAVING keeps everyone in the average and returns only departments whose overall average exceeds 50000.

open as a page

Why does COUNT(DISTINCT o.order_id) repair a fanned-out count but SUM(DISTINCT o.amount) not repair the total?

level: middleimportance: should knowfreq 50%

basics

~20 s

COUNT(DISTINCT pk) works because a primary key identifies a row uniquely, so collapsing repeats restores the true row count. SUM(DISTINCT amount) deduplicates values, not rows: two different orders of 100 collapse into a single 100.

open as a page

How do you check that a join did not multiply rows before trusting a SUM over it?

level: middleimportance: should knowfreq 45%

basics

~20 s

Compare the joined row count with the parent's: if COUNT() exceeds COUNT(DISTINCT parent_pk), the join duplicated parent rows and every additive aggregate over parent columns is inflated. Probe the child's key with GROUP BY … HAVING COUNT() > 1 first.

open as a page

What does SELECT COUNT(*), SUM(total) FROM orders return when orders is empty?

level: middleimportance: should knowfreq 51%

basics

~20 s

Exactly one row, holding 0 and NULL. An aggregate query with no GROUP BY always produces one row; COUNT of an empty input is 0, while SUM of an empty input is NULL. Adding GROUP BY would return zero rows instead.

open as a page

How does SUM(base + bonus) differ from SUM(base) + SUM(bonus) when bonus is NULL?

level: middleimportance: should knowfreq 44%

basics

~20 s

A NULL bonus makes base + bonus NULL for that row, so the aggregate skips the row entirely and loses its base too. SUM(base) + SUM(bonus) drops only the missing bonus. Fix with SUM(base + COALESCE(bonus, 0)).

open as a page

Why can GROUP BY on a primary key let you select non-grouped columns of that table?

level: middleimportance: should knowfreq 40%

basics

~20 s

Grouping by a primary key gives each group at most one row of that table, so every other column of it has exactly one value per group. SQL:1999 permits selecting such functionally dependent columns without listing them.

open as a page

Does ARRAY_AGG skip NULLs the way STRING_AGG does, and what do they return for an empty group?

level: middleimportance: should knowfreq 38%

basics

~20 s

No. String aggregation ignores NULL inputs, so LISTAGG, STRING_AGG and GROUP_CONCAT produce no element for them; ARRAY_AGG keeps NULLs as array elements. Over a group with no rows at all, both return NULL rather than an empty string or empty array.

open as a page

Why can SUM over an INTEGER column overflow, and how do you prevent it portably?

level: seniorimportance: should knowfreq 30%

basics

~20 s

SUM derives its result type from its argument. Engines that keep INTEGER for INTEGER input can exceed that range on a large table and raise an arithmetic overflow. Prevent it by casting the argument to a wider type inside the aggregate.

open as a page

Why does putting the condition in WHERE instead of inside SUM(CASE …) drop zero-count groups?

level: seniorimportance: should knowfreq 42%

basics

~20 s

WHERE removes rows before grouping, so a customer whose orders were all filtered out has no rows left, forms no group, and produces no output row. A condition inside CASE keeps every row in the group and reports a 0 instead.

open as a page

How do you count distinct (customer_id, product_id) pairs portably across SQL engines?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Count the rows of a de-duplicating derived table: SELECT COUNT(*) FROM (SELECT DISTINCT customer_id, product_id FROM sales) d. A multi-argument COUNT(DISTINCT a, b) is not portable — several engines accept only one expression.

open as a page

Why does a GROUP BY report skip days with no sales, and how do you emit a row for every day?

level: seniorimportance: should knowfreq 45%

basics

~20 s

GROUP BY forms groups only from key values present in its input, so a day with no rows produces no group and no output row. To show every day you must supply the key values from elsewhere — typically a calendar table left-joined to the aggregated result.

open as a page

A 'customers with no orders' report uses HAVING COUNT(*) = 0 and returns nothing — why?

level: seniorimportance: should knowfreq 40%

basics

~20 s

GROUP BY creates a group only where rows exist, so a customer with no orders produces no group for HAVING to test. HAVING can only remove groups, never invent them, and every surviving group has COUNT(*) of at least 1.

open as a page

Why is AVG(o.amount) over a fanned-out join wrong in a way COUNT(DISTINCT) cannot fix?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Fan-out turns the average into one weighted by each parent's child count: orders with many items count many times. Both numerator and denominator are inflated by row-specific factors, so no DISTINCT wrapper repairs it — only removing the multiplication does.

open as a page

Should COALESCE go inside AVG(rating) or around it, and what changes?

level: seniorimportance: should knowfreq 38%

basics

~20 s

They answer different questions. AVG(COALESCE(rating, 0)) counts unrated rows as zero and lowers the average; COALESCE(AVG(rating), 0) leaves the average of rated rows untouched and only substitutes a value when there is no data at all.

open as a page

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

level: seniorimportance: should knowfreq 45%

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.

open as a page

Why does a GROUP_CONCAT report silently lose values for the largest groups?

level: seniorimportance: should knowfreq 32%

basics

~20 s

The engine caps the aggregated result's length. MySQL truncates GROUP_CONCAT at group_concat_max_len — 1024 bytes by default — emitting only a warning. Raise the limit for known-bounded lists, or return rows or an array instead of one unbounded string.

open as a page

Can GROUP BY reference a SELECT-list alias or an ordinal position?

level: middleimportance: nice to knowfreq 35%

basics

~20 s

Standard SQL says no: GROUP BY is evaluated before the select list is projected, so output aliases do not exist yet and you must repeat the expression. Several engines allow aliases or ordinals as an extension.

open as a page

How do you de-duplicate values inside STRING_AGG, and why can DISTINCT plus ORDER BY fail?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

Where the engine allows it, put DISTINCT inside the aggregate: string_agg(DISTINCT tag, ', ' ORDER BY tag). With DISTINCT the sort key must be the aggregated expression itself, so ordering by another column is rejected. The portable fallback aggregates over a SELECT DISTINCT derived table.

open as a page

Where may a FILTER (WHERE …) clause legally attach — after COUNT(DISTINCT x), before OVER, or to ROW_NUMBER()?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

The filter clause belongs to aggregate-function syntax: it goes after the closing parenthesis of the argument list, so COUNT(DISTINCT x) FILTER (WHERE c) is legal and DISTINCT stays inside. With OVER it sits before it. ROW_NUMBER() is not an aggregate and takes no filter.

open as a page

In a ROLLUP report, why doesn't the grand-total AVG equal the average of the subtotal AVGs?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Each grouping set is computed independently from the base rows, so the grand-total AVG is the sum of all values over the count of all rows. Averaging the subtotal averages instead weights every group equally.

open as a page

showing 31–56 of 56