When a report needs one row per department, per month, or per product—not one row per employee—you use GROUP BY in DB2 for z/OS. Grouping collapses many detail rows into summary rows. Column functions such as COUNT, SUM, and AVG then run once per group. HAVING is the filter that keeps or drops those groups after the aggregates exist. This page covers GROUP BY, HAVING, aggregates with grouping, and the SELECT-list column rules that cause the famous SQLCODE -122.
A subselect is processed in a fixed logical order. You do not have to memorize optimizer internals, but you do need this sequence so WHERE versus HAVING makes sense:
| Clause | What it does |
|---|---|
| FROM | Build the working table (joins, table refs) |
| WHERE | Keep individual rows where the search condition is TRUE |
| GROUP BY | Partition remaining rows into groups |
| HAVING | Keep groups where the search condition is TRUE |
| SELECT | Compute the output columns for each surviving group |
| ORDER BY / OFFSET / FETCH | Sort and limit the final result (outer query) |
WHERE never sees groups. HAVING never sees discarded detail rows. If you filter WORKDEPT = 'A00', that belongs in WHERE so Db2 never groups the other departments. If you filter COUNT(*) > 10, that belongs in HAVING because the count does not exist until after grouping.
The GROUP BY clause specifies an intermediate result that consists of groups of rows from the previous clause (normally WHERE, or FROM if there is no WHERE). In the simplest form you list one or more grouping-expressions:
123SELECT WORKDEPT, COUNT(*) AS EMP_COUNT, AVG(SALARY) AS AVG_SAL FROM DSN8C10.EMP GROUP BY WORKDEPT;
All employees with the same WORKDEPT become one group. The result has one row per distinct department value (plus one extra group if any WORKDEPT values are null). COUNT(*) and AVG(SALARY) are evaluated using only the rows of that group.
Several grouping-expressions mean “same combination of values.” Department and job together is a different grouping from department alone:
1234SELECT WORKDEPT, JOB, COUNT(*) AS N, SUM(SALARY) AS PAYROLL FROM DSN8C10.EMP GROUP BY WORKDEPT, JOB ORDER BY WORKDEPT, JOB;
A00/MANAGER and A00/ANALYST are different groups. Order of columns in GROUP BY does not sort the output; use ORDER BY if you need a report sequence. GROUP BY only defines which rows share a bucket.
Each column name in a grouping-expression must unambiguously identify a column of the previous result (no ambiguous names after a join). IBM documents these restrictions on a grouping-expression:
To group on something that is illegal as a grouping-expression (for example a scalar subquery), first materialize it as a column in a nested table expression or common table expression, then GROUP BY that column.
123456SELECT DECADE, COUNT(*) AS N FROM ( SELECT YEAR(HIREDATE) / 10 * 10 AS DECADE FROM DSN8C10.EMP ) AS H GROUP BY DECADE;
You may group by an expression such as YEAR(HIREDATE) or COL1 + COL2. If the grouping-expression is COL1+COL2, the select list may use that same expression, and even COL1+COL2+3, because every row in the group has the same COL1+COL2. Associativity matters: 3+COL1+COL2 is not automatically accepted unless parentheses make the evaluation order match, for example 3+(COL1+COL2). For concatenation, IBM requires the grouping-expression to appear exactly as written in the select list.
123SELECT YEAR(HIREDATE) AS YR, COUNT(*) AS HIRES FROM DSN8C10.EMP GROUP BY YEAR(HIREDATE);
Repeating the expression in both SELECT and GROUP BY is the usual Db2 for z/OS style. You generally cannot GROUP BY the column alias YR; the alias is not a column of the FROM result.
For grouping, all null values of a grouping-expression are treated as equal. They form one group, not one group per null row. That is different from “unknown” in predicates, where null compared with null is not TRUE. If WORKDEPT is null for three employees, those three rows share a group and COUNT(*) for that group is 3. The grouping key shown in the result is the null value.
A column function (aggregate) takes values from the current group and returns one value. Without GROUP BY, the whole intermediate result is one group (and HAVING, if present, applies to that single group). With GROUP BY, each group supplies the argument rows.
| Function | Meaning in a group |
|---|---|
| COUNT(*) | Number of rows in the group, including rows with null columns |
| COUNT(col) | Number of non-null values of col in the group |
| COUNT(DISTINCT col) | Number of distinct non-null values of col |
| SUM(expr) | Total of non-null numeric values; null if all inputs are null |
| AVG(expr) | Average of non-null values (SUM / COUNT of those values) |
| MIN(expr) / MAX(expr) | Smallest / largest non-null value in the group |
| COUNT_BIG(*) | Like COUNT(*) but BIGINT result for very large groups |
Null handling is the trap beginners hit. SUM, AVG, MIN, and MAX ignore null inputs. If every SALARY in the group is null, SUM and AVG return null—not zero. COUNT(*) still counts the rows. COUNT(SALARY) counts only non-null salaries. That is why a department of five people with two missing salaries can show COUNT(*) = 5 and COUNT(SALARY) = 3.
12345678910SELECT WORKDEPT, COUNT(*) AS ROWS_IN_GROUP, COUNT(COMM) AS HAVE_COMM, COUNT(DISTINCT JOB) AS DISTINCT_JOBS, SUM(SALARY) AS PAYROLL, AVG(SALARY) AS AVG_SAL, MIN(SALARY) AS LOW, MAX(SALARY) AS HIGH FROM DSN8C10.EMP GROUP BY WORKDEPT;
DISTINCT inside an aggregate is allowed (COUNT DISTINCT, SUM DISTINCT, and so on) and means “use unique non-null values inside this group.” It is not the same as SELECT DISTINCT on the whole query.
After GROUP BY or HAVING is in play, every select-list item that is not a column function must be a grouping-expression (or be built from grouping-expressions and constants in the way IBM allows). In plain language:
| Select-list item | Allowed? | Notes |
|---|---|---|
| Grouping column or grouping-expression | Yes | Same value for every row in the group |
| Column function (COUNT, SUM, AVG, MIN, MAX, …) | Yes | Computed from the rows of that group |
| Expression of grouping-expressions and constants | Yes | Must match grouping expression shape (parentheses matter) |
| Literal or special register | Yes | Same constant on every output row |
| Bare non-grouping column (for example EMPNO) | No | SQLCODE -122 / SQLSTATE 42803 |
123456789-- Invalid: EMPNO is not grouped and not aggregated SELECT EMPNO, WORKDEPT, AVG(SALARY) FROM DSN8C10.EMP GROUP BY WORKDEPT; -- Valid: only grouping columns and aggregates SELECT WORKDEPT, AVG(SALARY) AS AVG_SAL FROM DSN8C10.EMP GROUP BY WORKDEPT;
The invalid form is the classic “I want the average salary and also the employee number.” One group has many EMPNO values. Db2 refuses to pick one at random. That is SQLCODE -122 (column or expression in the SELECT list is not valid), typically SQLSTATE 42803. Fix it by grouping on EMPNO as well (usually wrong for a department average), dropping EMPNO, or using a different query pattern such as a join to a grouped nested table.
SELECT * is almost never valid with GROUP BY unless every selected column is a grouping column—which would make aggregates pointless. List the grouping columns and the functions you need.
A column that appears only inside a function does not have to be in GROUP BY. SALARY in AVG(SALARY) is an argument to a function, not a grouping key.
The HAVING clause specifies a result of those groups for which the search-condition is TRUE. The intermediate table is the result of the previous clause. If that clause is not GROUP BY, the intermediate result is treated as a single group with no grouping columns.
12345SELECT WORKDEPT, COUNT(*) AS N, AVG(SALARY) AS AVG_SAL FROM DSN8C10.EMP GROUP BY WORKDEPT HAVING COUNT(*) >= 5 AND AVG(SALARY) > 50000;
Each column name in the HAVING search condition must:
A correlated reference to the current group must identify a grouping column or sit inside a column function. HAVING can contain a subquery; conceptually the subquery runs for each group, but Db2 only needs to re-execute it per group when the subquery is correlated.
123456-- Groups whose average salary exceeds the company-wide average SELECT WORKDEPT, AVG(SALARY) AS DEPT_AVG FROM DSN8C10.EMP GROUP BY WORKDEPT HAVING AVG(SALARY) > (SELECT AVG(SALARY) FROM DSN8C10.EMP);
The inner AVG(SALARY) here is uncorrelated: it does not mention WORKDEPT from the outer group, so it can be computed once. Putting WORKDEPT = some outer column inside the subquery would make it correlated.
You can write HAVING with no GROUP BY. All rows form one group. The select list may be column functions, literals, special registers, or correlated references—not a bare non-grouped column. This pattern is a “filter the grand total” query:
123SELECT COUNT(*) AS N, AVG(SALARY) AS AVG_SAL FROM DSN8C10.EMP HAVING COUNT(*) > 0;
If the HAVING condition is false (for example COUNT(*) = 0 on an empty table after WHERE), you get an empty result, not a row of zeros. That surprises people who expected a single summary row always.
Both clauses use search conditions that evaluate to TRUE, FALSE, or UNKNOWN. Only TRUE rows or groups survive. The difference is when they run and what they may reference:
123456789-- WHERE: drop detail rows first (cheaper, clearer) SELECT WORKDEPT, AVG(SALARY) AS AVG_SAL FROM DSN8C10.EMP WHERE JOB <> 'PRES' GROUP BY WORKDEPT HAVING AVG(SALARY) > 45000; -- Do not write: WHERE AVG(SALARY) > 45000 -- invalid
You can put a grouping-column test in HAVING (HAVING WORKDEPT = 'A00'), but that forces Db2 to group every department and then throw most groups away. Prefer WHERE for that filter. Use HAVING for conditions that need the aggregate.
Three-valued logic still applies. HAVING AVG(COMM) > 0 drops groups where AVG(COMM) is null (all commissions null) because UNKNOWN is not TRUE. If you need those groups, test with COALESCE or COUNT(COMM).
GROUP BY does not sort. Add ORDER BY on grouping columns, aliases, or column numbers. You may also order by an aggregate:
1234SELECT WORKDEPT, SUM(SALARY) AS PAYROLL FROM DSN8C10.EMP GROUP BY WORKDEPT ORDER BY PAYROLL DESC;
OFFSET and FETCH FIRST apply after grouping and HAVING, so FETCH FIRST 5 ROWS ONLY on this query means “five department totals,” not “five employees.”
Join first, then filter, then group. If one department has three employees and each employee has two project rows, a join to the project table multiplies rows before GROUP BY. SUM(SALARY) would then over-count salary unless you aggregate in stages (group employees first, then join) or use a different grain. Always ask: “What is one group, and which table is the fact table?”
SELECT DISTINCT and GROUP BY can look similar when there are no aggregates: both collapse duplicate combinations. DISTINCT cannot compute SUM. Prefer GROUP BY when you need totals. Prefer DISTINCT when you only need unique projected rows and no functions.
Imagine a pile of toy blocks. Each block is an employee. GROUP BY is sorting the blocks into buckets by color (department). Then you count how many blocks are in each bucket, or weigh them. HAVING is looking at the buckets and keeping only the heavy ones. WHERE is throwing away some blocks before you sort them into buckets—like removing the broken toys first so they never get counted.
1. What does GROUP BY do in a Db2 subselect?
2. Which SELECT list is valid with GROUP BY WORKDEPT?
3. When is HAVING applied relative to WHERE?
4. How does Db2 treat nulls in a grouping column?
5. What happens if you write HAVING without GROUP BY?