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 pageshowhide
explore
- GROUP BY Semantics6 questions
- The Every-Selected-Column Rule4 questions
- Core Aggregate Functions6 questions
- Aggregates and NULLs5 questions
- COUNT(*) vs COUNT(col) vs COUNT(DISTINCT)5 questions
- HAVING vs WHERE5 questions
- GROUPING SETS, ROLLUP, and CUBE5 questions
- The FILTER Clause5 questions
- Conditional Aggregation with CASE5 questions
- String and Array Aggregation5 questions
- Aggregating over Joins (Fan-out)5 questions
- AI & Data Scientistrole
- AI Engineerrole
- BI Analystrole
- Backend Developerrole
- Cyber Security Expertrole
- Data Analystrole
- Data Engineerrole
- Full Stack Developerrole
- Java Backend Developerrole
- Java SDETrole
- Kotlin Backend Developerrole
- MLOps Engineerrole
- Machine Learning Engineerrole
- PostgreSQL DBArole
- QA Engineerrole
- SQLskill
questions
page 2 of 2Can GROUP BY take an expression instead of a column, and how do you bucket a timestamp by day?
basics
~20 sYes — 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.
Why does SELECT COUNT(*), SUM(amount) FROM orders WHERE region = 'ZZ' return a row when nothing matches?
basics
~20 sWith 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.
What does GROUP BY do when the SELECT list has no aggregate, and how does it compare with SELECT DISTINCT?
basics
~20 sGrouping 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.
How do GROUP BY ROLLUP(a, b, c) and GROUP BY CUBE(a, b, c) differ in the grouping sets they generate?
basics
~20 sROLLUP 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.
How would you return per-region, per-product and overall totals in one query using GROUPING SETS?
basics
~10 sWrite 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.
What does a HAVING clause do in a query that has no GROUP BY?
basics
~10 sWithout 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.
In a per-department salary query, how does WHERE salary > 50000 differ from HAVING AVG(salary) > 50000?
basics
~20 sWHERE 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.
Why does COUNT(DISTINCT o.order_id) repair a fanned-out count but SUM(DISTINCT o.amount) not repair the total?
basics
~20 sCOUNT(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.
How do you check that a join did not multiply rows before trusting a SUM over it?
basics
~20 sCompare 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.
What does SELECT COUNT(*), SUM(total) FROM orders return when orders is empty?
basics
~20 sExactly 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.
How does SUM(base + bonus) differ from SUM(base) + SUM(bonus) when bonus is NULL?
basics
~20 sA 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)).
Why can GROUP BY on a primary key let you select non-grouped columns of that table?
basics
~20 sGrouping 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.
Does ARRAY_AGG skip NULLs the way STRING_AGG does, and what do they return for an empty group?
basics
~20 sNo. 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.
Why can SUM over an INTEGER column overflow, and how do you prevent it portably?
basics
~20 sSUM 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.
Why does putting the condition in WHERE instead of inside SUM(CASE …) drop zero-count groups?
basics
~20 sWHERE 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.
How do you count distinct (customer_id, product_id) pairs portably across SQL engines?
basics
~10 sCount 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.
Why does a GROUP BY report skip days with no sales, and how do you emit a row for every day?
basics
~20 sGROUP 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.
A 'customers with no orders' report uses HAVING COUNT(*) = 0 and returns nothing — why?
basics
~20 sGROUP 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.
Why is AVG(o.amount) over a fanned-out join wrong in a way COUNT(DISTINCT) cannot fix?
basics
~20 sFan-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.
Should COALESCE go inside AVG(rating) or around it, and what changes?
basics
~20 sThey 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.
What breaks when you wrap extra columns in MIN() just to satisfy the GROUP BY rule?
basics
~20 sEach 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.
Why does a GROUP_CONCAT report silently lose values for the largest groups?
basics
~20 sThe 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.
Can GROUP BY reference a SELECT-list alias or an ordinal position?
basics
~20 sStandard 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.
How do you de-duplicate values inside STRING_AGG, and why can DISTINCT plus ORDER BY fail?
basics
~20 sWhere 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.
Where may a FILTER (WHERE …) clause legally attach — after COUNT(DISTINCT x), before OVER, or to ROW_NUMBER()?
basics
~20 sThe 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.
In a ROLLUP report, why doesn't the grand-total AVG equal the average of the subtotal AVGs?
basics
~20 sEach 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.
showing 31–56 of 56