Skip to content

MySQL - Having Clause

In MySQL, the HAVING clause is used to filter the results of a query based on grouped data. It is applied after the data has been grouped using the GROUP BY clause and after any aggregate functions (like COUNT(), SUM(), AVG()) have been calculated.

This makes it fundamentally different from the WHERE clause. The WHERE clause filters individual rows before they are grouped, whereas the HAVING clause filters entire groups of rows after they have been aggregated.

Understanding the Difference: WHERE vs. HAVING

Section titled “Understanding the Difference: WHERE vs. HAVING”

The key distinction lies in the order of query execution. MySQL processes a query in a logical sequence:

  1. FROM / JOIN: Gathers the initial set of rows.
  2. WHERE: Filters individual rows based on conditions.
  3. GROUP BY: Aggregates the filtered rows into groups.
  4. HAVING: Filters the newly created groups based on conditions.
  5. SELECT: Selects the final columns.
  6. ORDER BY: Sorts the final result set.
  7. LIMIT: Restricts the number of rows returned.

Because WHERE is processed before GROUP BY, you cannot use an aggregate function in a WHERE clause. You must use HAVING for that purpose.

The basic syntax for the HAVING clause is as follows:

SELECT
column_name(s),
aggregate_function(column_name)
FROM
table_name
WHERE
condition(s) -- Optional: Filters rows before grouping
GROUP BY
column_name(s)
HAVING
aggregate_condition(s); -- Filters groups after aggregation
ORDER BY
column_name(s); -- Optional: Sorts the final result

Let’s set up a realistic table to work with. We’ll create an employees table to store employee data.

-- Create the employees table
CREATE TABLE employees (
id INT AUTO_INCREMENT PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
department VARCHAR(50) NOT NULL,
salary DECIMAL(10, 2) NOT NULL,
hire_date DATE NOT NULL
);
-- Insert sample data
INSERT INTO employees (first_name, last_name, department, salary, hire_date) VALUES
('Ayla', 'Chen', 'Engineering', 95000.00, '2021-06-15'),
('Ben', 'Carter', 'Engineering', 110000.00, '2020-02-01'),
('Chloe', 'Davis', 'Sales', 72000.00, '2022-08-20'),
('David', 'Miller', 'Sales', 68000.00, '2021-11-10'),
('Emma', 'Wilson', 'HR', 65000.00, '2023-01-05'),
('Finn', 'Rodriguez', 'Engineering', 125000.00, '2019-09-01'),
('Grace', 'Lee', 'Sales', 88000.00, '2020-05-25');

Let’s verify the contents of our table:

SELECT * FROM employees;
idfirst_namelast_namedepartmentsalaryhire_date
1AylaChenEngineering95000.002021-06-15
2BenCarterEngineering110000.002020-02-01
3ChloeDavisSales72000.002022-08-20
4DavidMillerSales68000.002021-11-10
5EmmaWilsonHR65000.002023-01-05
6FinnRodriguezEngineering125000.002019-09-01
7GraceLeeSales88000.002020-05-25

Let’s find departments that have more than one employee.

SELECT
department,
COUNT(id) AS number_of_employees
FROM
employees
GROUP BY
department
HAVING
COUNT(id) > 1;

Output:

departmentnumber_of_employees
Engineering3
Sales3

Now, let’s find departments where the total payroll (sum of salaries) exceeds $250,000.

SELECT
department,
SUM(salary) AS total_payroll
FROM
employees
GROUP BY
department
HAVING
SUM(salary) > 250000;

Output:

departmenttotal_payroll
Engineering330000.00

We can also filter for departments where the average salary is less than $100,000.

SELECT
department,
AVG(salary) as average_salary
FROM
employees
GROUP BY
department
HAVING
AVG(salary) < 100000;

Output:

departmentaverage_salary
Sales76000.00
HR65000.00

You can use both WHERE and HAVING in the same query. For example, let’s find departments with more than one employee, but only considering employees hired since 2021.

SELECT
department,
COUNT(id) AS new_hires
FROM
employees
WHERE
hire_date >= '2021-01-01' -- Filter rows BEFORE grouping
GROUP BY
department
HAVING
COUNT(id) > 1; -- Filter groups AFTER grouping

Output:

departmentnew_hires
Sales2
  • Using WHERE with aggregates: A query like SELECT department, COUNT(*) FROM employees WHERE COUNT(*) > 1; will fail. You must use HAVING for conditions on aggregate results.
  • Using non-aggregated columns in HAVING: A condition in HAVING must either be an aggregate function or be a column that is also in the GROUP BY clause. For example, HAVING salary > 80000 would be invalid unless salary was also in the GROUP BY list.
  • Confusing aliases: While MySQL non-standardly allows you to use SELECT list aliases in the HAVING clause (e.g., HAVING total_payroll > 250000), this is not portable to other SQL databases like PostgreSQL or SQL Server. It’s best practice to repeat the aggregate function for better compatibility: HAVING SUM(salary) > 250000.

Using HAVING in an Application (Python Example)

Section titled “Using HAVING in an Application (Python Example)”

In a real-world application, you would execute these queries from your backend code. Here is a complete, modern Python example using the mysql-connector-python library. This demonstrates best practices like using prepared statements and managing credentials securely.

First, ensure you have the necessary library and a way to manage secrets:
1. **Install libraries:** `pip install mysql-connector-python python-dotenv`
2. **Create a `.env` file** in your project directory to store your database credentials. Never hard-code them in your script.
```.env
DB_HOST=localhost
DB_USER=your_user
DB_PASSWORD=your_password
DB_NAME=your_db_name
Now, here is the Python script to find departments with an average salary above a certain threshold.
```python
import mysql.connector
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
def find_high_paying_departments(min_avg_salary):
"""Finds departments with an average salary above a given threshold."""
departments = []
try:
# Establish a secure connection
with mysql.connector.connect(
host=os.getenv('DB_HOST'),
user=os.getenv('DB_USER'),
password=os.getenv('DB_PASSWORD'),
database=os.getenv('DB_NAME')
) as conn:
with conn.cursor(dictionary=True) as cursor:
# The query uses GROUP BY and HAVING
query = ("""
SELECT
department,
ROUND(AVG(salary), 2) as average_salary
FROM
employees
GROUP BY
department
HAVING
AVG(salary) > %s
ORDER BY
average_salary DESC
""")
# Use a tuple for parameters to prevent SQL injection
cursor.execute(query, (min_avg_salary,))
departments = cursor.fetchall()
except mysql.connector.Error as e:
print(f"Error connecting to MySQL or executing query: {e}")
return None
return departments
if __name__ == "__main__":
# Find departments with an average salary greater than $80,000
high_paying_depts = find_high_paying_departments(80000)
if high_paying_depts is not None:
if high_paying_depts:
print("Found high-paying departments:")
for dept in high_paying_depts:
print(f"- Department: {dept['department']}, Average Salary: ${dept['average_salary']}")
else:
print("No departments met the criteria.")

This script is robust, secure, and demonstrates the practical application of the HAVING clause to generate business insights.