Database systems · Section 4.9 · Worked examples

More aggregate function examples: the employee database

The Chapter 4 review introduces COUNT, SUM, AVG, MIN, MAX, GROUP BY, and HAVING with the PRODUCT table. This page works through the same tools again on a payroll style question: who gets paid what, by department and by job title.

The employee database

EMPLOYEES holds 24 rows, one per person, with a salary, a dept_id, and a job_id. Every aggregate example on this page is really the same question asked a different way: how does pay break down once you group people together?

The three tables behind every query on this page.
TableRowsRelevant columns
DEPARTMENT6dept_id, dept_name, location, budget
JOBS6job_id, job_title, min_salary, max_salary
EMPLOYEES24emp_id, first_name, last_name, email, salary, dept_id, job_id

Build it yourself. Download employee_db.py, save it next to a notebook, then run from employee_db import create_database followed by connection, cursor = create_database(). Every number quoted on this page comes straight out of that data.

More Section 4.9 examples

Aggregates, again, on payroll data

Same five functions, same GROUP BY rule, same WHERE-before-HAVING-after order. What is new here is how visibly the null-handling rule and the grouping rule change the answer.

COUNT(*) versus COUNT(column)

Three employees, Mei Chen, Grace Adeyemi, and Omar Farouk, have no email address on file.

SELECT COUNT(*), COUNT(email)
FROM   EMPLOYEES;
-- COUNT(*) is 24, every row counts; COUNT(email) is 21, the three NULL emails are skipped

SUM, AVG, MIN, and MAX

SELECT SUM(salary), AVG(salary), MIN(salary), MAX(salary)
FROM   EMPLOYEES;
-- total payroll $1,865,000; average $77,708.33; low $47,000 (Grace Adeyemi); high $145,000 (Carolyn Reyes, the president)

GROUP BY department

Grouping straight off EMPLOYEES only shows departments that actually have a row to group.

SELECT D.dept_name, COUNT(*) AS headcount, ROUND(AVG(E.salary), 2) AS avg_salary
FROM   EMPLOYEES E JOIN DEPARTMENT D ON E.dept_id = D.dept_id
GROUP BY D.dept_name
ORDER BY avg_salary DESC;
Result: 4 rows, one per department that has employees.
dept_nameheadcountavg_salary
Accounting589,800.00
Engineering985,111.11
Sales765,571.43
Human Resources363,666.67

Marketing and Research never appear in that result, and no HAVING clause is hiding them. GROUP BY can only group rows that exist; a department with zero rows in EMPLOYEES has nothing to collect into a group. Finding the zero-employee departments is an outer join question, not a GROUP BY question — see the JOIN examples page for that version of it.

GROUP BY job title

SELECT J.job_title, COUNT(*) AS headcount, AVG(E.salary) AS avg_salary
FROM   EMPLOYEES E JOIN JOBS J ON E.job_id = J.job_id
GROUP BY J.job_title
ORDER BY avg_salary DESC;
Result: 6 rows, one per job title.
job_titleheadcountavg_salary
President1145,000.00
Manager491,000.00
Software Engineer883,000.00
Accountant372,000.00
Sales Rep660,166.67
HR Specialist257,500.00

HAVING: filtering the groups

SELECT D.dept_name, COUNT(*) AS headcount
FROM   EMPLOYEES E JOIN DEPARTMENT D ON E.dept_id = D.dept_id
GROUP BY D.dept_name
HAVING COUNT(*) > 5;
-- only Engineering qualifies, with 9 employees; every other department has 7 or fewer

WHERE and HAVING together

Which departments have more than one employee earning over $70,000?

SELECT D.dept_name, COUNT(*) AS high_earners
FROM   EMPLOYEES E JOIN DEPARTMENT D ON E.dept_id = D.dept_id
WHERE  E.salary > 70000
GROUP BY D.dept_name
HAVING COUNT(*) > 1
ORDER BY high_earners DESC;
Result: 3 rows. Human Resources drops out here.
dept_namehigh_earners
Engineering7
Accounting4
Sales2

Human Resources has exactly one employee, Elena Castillo, earning over $70,000, so WHERE keeps that one row, GROUP BY forms a one-row group for Human Resources, and HAVING COUNT(*) > 1 then throws that whole group away. WHERE decided which rows were even in the running; HAVING decided which finished groups were big enough to report.