Lab 04: Group Functions¶
Objectives:¶
- Use aggregate (group) functions
AVG,SUM,COUNT,MIN, andMAXto summarize data across rows. - Group rows into subsets using
GROUP BYand compute an aggregate per group. - Filter groups using
HAVING, and understand how it differs fromWHERE. - Produce subtotal and grand-total reports using SQL Server's
ROLLUPandCUBEextensions. - Build a single comma-separated summary string per group using
STRING_AGG.
Activity Outcomes:¶
- Compute summary statistics over an entire table or over groups of rows.
- Correctly choose between
WHERE(row filter) andHAVING(group filter). - Produce a grouped report with subtotals using
ROLLUP. - Aggregate text values from multiple rows into a single delimited string per group.
Tools / Software Required:
- SQL Server 2019 (Developer or Express edition)
- SQL Server Management Studio (SSMS) 18 or later
- The HR sample database, restored and attached to the local SQL Server instance
Instructor Note: As pre-lab activity, read the chapter on "Group Functions and the GROUP BY / HAVING Clauses" from Bryan Syverson and Joel Murach's "Murach's SQL Server for Developers".
1) Useful Concepts¶
| Function / Clause | Description |
|---|---|
AVG(col) |
Returns the average of a numeric column, ignoring NULL values |
SUM(col) |
Returns the total of a numeric column, ignoring NULL values |
COUNT(*) |
Returns the number of rows in a group (or table) |
COUNT(col) |
Returns the number of non-NULL values in col |
COUNT(DISTINCT col) |
Returns the number of distinct non-NULL values in col |
MIN(col) / MAX(col) |
Returns the smallest / largest value in col |
GROUP BY col1, col2 |
Groups rows that share the same values in the listed columns; every non-aggregated column in the SELECT list must appear in GROUP BY |
HAVING condition |
Filters out groups after aggregation, based on a condition involving an aggregate function |
WHERE vs HAVING |
WHERE filters individual rows before grouping/aggregation happens and cannot reference an aggregate function; HAVING filters groups after aggregation and is the only place an aggregate function can appear in a filter condition |
GROUP BY col WITH ROLLUP |
Adds subtotal rows for each group plus one grand-total row, in hierarchical order |
GROUP BY col1, col2 WITH CUBE |
Adds subtotal rows for every possible combination of the grouping columns, plus a grand total |
STRING_AGG(col, separator) |
Concatenates the (non-NULL) values of col across a group into one delimited string (SQL Server 2017+) |
2) Solved Lab Activities¶
| Sr. No. | Allocated Time | Level of Complexity | CLO Mapping |
|---|---|---|---|
| Activity 1 | 10 Minutes | Low | CLO-1 |
| Activity 2 | 15 Minutes | Medium | CLO-1 |
| Activity 3 | 25 Minutes | High | CLO-1 |
| Activity 4 | 20 Minutes | Medium | CLO-1 |
Activity 1: Simple aggregate summary¶
Display the total number of employees, the average salary, the minimum salary, and the maximum salary across the entire EMPLOYEES table.
Solution:
SELECT COUNT(*) AS "Employee Count",
AVG(salary) AS "Average Salary",
MIN(salary) AS "Minimum Salary",
MAX(salary) AS "Maximum Salary"
FROM EMPLOYEES;
Output / Expected behaviour:
| Employee Count | Average Salary | Minimum Salary | Maximum Salary |
|---|---|---|---|
| 42 | 8456.33 | 2500.00 | 24000.00 |
Activity 2: GROUP BY with HAVING¶
Display department_id, the number of employees in that department, and the average salary per department, but only for departments with more than 3 employees. Order the result by average salary descending.
Solution:
SELECT department_id AS "Department",
COUNT(*) AS "Employee Count",
AVG(salary) AS "Average Salary"
FROM EMPLOYEES
GROUP BY department_id
HAVING COUNT(*) > 3
ORDER BY "Average Salary" DESC;
Output / Expected behaviour:
| Department | Employee Count | Average Salary |
|---|---|---|
| 90 | 4 | 15750.00 |
| 60 | 5 | 7200.00 |
| 50 | 12 | 3475.50 |
Note how this differs from using WHERE COUNT(*) > 3, which is illegal — WHERE cannot reference an aggregate because it runs before grouping; HAVING runs after aggregation and is the correct clause here.
Activity 3: ROLLUP subtotal report¶
Produce a report that shows, for each department_id and job_id combination, the total salary paid (SUM), plus a subtotal row per department and a grand-total row for the whole company.
Solution:
SELECT department_id AS "Department",
job_id AS "Job",
SUM(salary) AS "Total Salary"
FROM EMPLOYEES
GROUP BY department_id, job_id
WITH ROLLUP
ORDER BY department_id, job_id;
Output / Expected behaviour:
| Department | Job | Total Salary |
|---|---|---|
| 60 | IT_PROG | 28800.00 |
| 60 | NULL | 28800.00 |
| 90 | AD_PRES | 24000.00 |
| 90 | AD_VP | 34000.00 |
| 90 | NULL | 58000.00 |
| NULL | NULL | 355167.86 |
The rows where Job is NULL are the per-department subtotal rows produced by ROLLUP; the single row where both Department and Job are NULL is the grand total for the entire company.
Activity 4: STRING_AGG — employee roster per department¶
Produce a single row per department showing department_id and one column named "Employee Roster" containing every employee's last name in that department, comma-separated, in alphabetical order.
Solution:
SELECT department_id AS "Department",
STRING_AGG(last_name, ', ') WITHIN GROUP (ORDER BY last_name) AS "Employee Roster"
FROM EMPLOYEES
GROUP BY department_id
ORDER BY department_id;
Output / Expected behaviour:
| Department | Employee Roster |
|---|---|
| 60 | Austin, Ernst, Hunold, Lorentz, Pataballa |
| 90 | De Haan, King, Kochhar |
| 100 | Faviet, Greenberg, Popp, Urman |
3) Graded Lab Tasks¶
Note: The instructor may adjust these tasks to the level of difficulty and complexity of the solved activities. Tasks should be evaluated in the same lab session.
Lab Task 1: Job-wise salary summary
Write a query against EMPLOYEES that displays job_id, COUNT(*) of employees holding that job, and SUM(salary), MIN(salary), and MAX(salary) for each job_id. Group by job_id and order by SUM(salary) descending.
Lab Task 2: High-cost departments only
Write a query against EMPLOYEES that displays department_id and the total salary bill (SUM) per department, filtering with WHERE to exclude any employee hired before '2015-01-01' from the calculation, and then using HAVING to show only departments whose total salary bill (after the WHERE filter) exceeds 20000. Explain, as a comment in your script, why the date filter must go in WHERE and not HAVING.
Lab Task 3: Department and job CUBE report with a comma-separated name list
Write a query against EMPLOYEES that groups by department_id and job_id using WITH CUBE, showing COUNT(*) and AVG(salary) for every combination (including the all-department and all-job subtotal rows). As a second, separate query, use STRING_AGG to produce one row per job_id listing every distinct department_id (CAST to VARCHAR) that has an employee in that job, comma-separated.