在SQL数据查询中,筛选是重要操作。WHERE和HAVING子句作为筛选工具,功能虽然相似,但适用场景和底层逻辑差异明显。下面从基础语法出发,结合场景示例、性能优化、面试考点等方面,详细对比分析WHERE和HAVING子句,仅供参考。
一、基础语法
1. WHERE子句:行级筛选的“前置过滤器”
定义:WHERE子句用在数据分组、聚合之前,对表中的单个行进行筛选,仅保留满足条件的行,参与后续操作(如查询、分组、聚合等)。
语法结构:
-- 单表查询
SELECT 列名1, 列名2, ...
FROM 表名
WHERE 行级条件表达式;
-- 多表关联查询(可结合JOIN使用)
SELECT 表1.列名, 表2.列名
FROM 表1
JOIN 表2 ON 关联条件
WHERE 行级条件表达式;
支持的条件类型:
- 比较运算符:
=
(等于)、!=
/<>
(不等于)、>
(大于)、<
(小于)、>=
(大于等于)、<=
(小于等于) - 逻辑运算符:
AND
(多条件同时满足)、OR
(多条件满足其一)、NOT
(否定条件) - 范围匹配:
BETWEEN...AND...
(闭区间范围)、IN
(匹配集合中的值) - 模糊匹配:
LIKE
(配合%
通配符匹配任意字符,_
匹配单个字符)
2. HAVING子句:分组级筛选的“后置过滤器”
定义:HAVING子句仅用在GROUP BY
分组之后,对分组结果进行筛选,筛选条件可基于聚合函数(如SUM()
、COUNT()
等)。
语法结构:
SELECT 分组列, 聚合函数(列名) AS 别名
FROM 表名
[WHERE 行级条件] -- 可选,先筛选行
GROUP BY 分组列 -- 对行分组
HAVING 聚合条件 -- 筛选分组结果
[ORDER BY 列名]; -- 可选,排序
支持的条件类型:
- 聚合函数:
COUNT(列名)
(计数)、SUM(列名)
(求和)、AVG(列名)
(平均值)、MAX(列名)
(最大值)等 - 分组列:
GROUP BY
中指定的列名(不可使用未分组的列)
说明:
- 在部分数据库(如 MySQL)中,即使不使用GROUP BY,HAVING也可单独使用(此时默认将所有数据视为一个分组)。例如:
-- 不使用GROUP BY,HAVING筛选聚合结果(默认全表为一组)
SELECT COUNT(*) AS total FROM employees HAVING COUNT(*) > 10;
- HAVING中可直接使用SELECT中定义的聚合函数别名(部分数据库支持)。例如:
-- 部分数据库支持(如MySQL):HAVING使用SELECT中的聚合别名
SELECT department, AVG(salary) AS avg_sal
FROM employees
GROUP BY department
HAVING avg_sal > 8000; -- 直接使用别名avg_sal
并非所有数据库都支持(如 Oracle 要求HAVING中重复写聚合函数)
二、WHERE和HAVING子句差异对比
在SQL中,WHERE和HAVING子句都用于筛选数据,但它们在使用场景、作用对象、语法规则和性能等方面区别明显,具体如下:
1. 使用场景
- WHERE:主要用于对表进行查询操作时,对表中的行进行筛选,在分组和聚合操作之前起作用,用于选择满足特定条件的单个行。
-- 示例:从employees表中筛选出工资大于5000的员工:
SELECT * FROM employees WHERE salary > 5000;
- HAVING:通常用在分组(GROUP BY)操作之后,用于对分组后的结果进行筛选,基于聚合函数的结果来筛选分组。
-- 示例:找出平均工资大于8000的部门:
SELECT department, AVG(salary) AS avg_salary FROM employees GROUP BY department HAVING AVG(salary) > 8000;
2. 作用对象
- WHERE:作用于基本表或视图中的每一行数据,也就是未分组的原始数据,根据指定条件决定是否将该行包含在结果集中。它不能直接与聚合函数一起使用,因为聚合函数是在分组之后对一组数据进行计算,而WHERE在分组之前执行。
- HAVING:作用于由GROUP BY子句创建的分组,也就是分组后的聚合数据,根据聚合函数计算后的结果来决定是否保留该分组,主要用于筛选基于聚合结果的分组数据。
3. 语法规则
- WHERE:紧跟在FROM子句之后、GROUP BY之前,语法结构为:SELECT 列名 FROM 表名 WHERE 条件表达式; ,条件表达式可以是比较运算符(如>、<、=)、逻辑运算符(如AND、OR)等组成的表达式,且不能包含聚合函数。
- HAVING:紧跟在GROUP BY子句之后、ORDER BY之前,语法结构为:SELECT 列名 FROM 表名 GROUP BY 分组列名 HAVING 条件表达式; ,这里的条件表达式通常基于聚合函数(如SUM、AVG、COUNT等)的结果,也可以使用GROUP BY中指定的列名。
-- 示例:找出员工数量大于5的部门:
SELECT department, COUNT(*) AS employee_count FROM employees GROUP BY department HAVING COUNT(*) > 5;
4. 筛选时机
- WHERE:在分组、聚合操作之前执行筛选,先对原始数据行进行过滤,再进行后续的分组和聚合。
- HAVING:在分组、聚合操作之后执行筛选,先完成数据分组和聚合计算,再对得到的分组结果进行过滤。
5. 性能差异
- WHERE:由于在数据分组之前对数据进行筛选,减少了参与后续操作的数据量,所以在性能上一般会优于HAVING。所以,能在WHERE中完成的筛选操作,尽量不要放到HAVING中。
- HAVING:需要先进行分组操作,然后再对分组结果进行筛选,相对而言性能可能会稍差一些。但当筛选条件依赖于聚合函数的结果时,就必须用HAVING。
小结
对比角度 | WHERE子句 | HAVING子句 |
---|
作用对象 | | |
使用位置 | | |
能否用聚合函数 | | |
筛选时机 | | |
适用场景 | | |
三、适用场景分析
为直观理解WHERE和HAVING子句差异,以下先通过“员工表(employees)”模拟数据进行对比演示。
表结构与数据:
场景1:使用WHERE筛选行(分组前)
需求:查询2020年及之后入职的员工姓名和薪资。
SELECT name, salary
FROM employees
WHERE hire_year >= 2020;
结果:
说明:WHERE直接过滤原始行,仅保留hire_year >= 2020
的记录,未涉及分组。
场景2:使用HAVING筛选分组(分组后)
需求:查询平均薪资大于等于10000的部门及平均薪资。
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
HAVING AVG(salary) >= 10000;
结果:
-- (6000+15000+9000)/3 = 10000
说明:先按部门分组计算平均薪资,再通过HAVING筛选出平均薪资>=10000的分组。
场景3:WHERE与HAVING结合使用
需求:查询2019年及之前入职的员工中,总薪资大于20000的部门及总薪资。
SELECT department, SUM(salary) AS total_salary
FROM employees
WHERE hire_year <= 2019 -- 先筛选2019年前入职的员工
GROUP BY department
HAVING SUM(salary) > 20000; -- 再筛选总薪资>20000的部门
分步解析:
- WHERE筛选:仅保留
hire_year <= 2019
的员工(李四、赵六); - 聚合:计算每组总薪资(技术部12000,销售部15000);
- HAVING筛选:总薪资>20000的分组(无符合条件的结果,故返回空)。
下面再通过另外一个完整示例将数据处理流程说明、各阶段逻辑、对应 SQL 代码以及各阶段数据示例完整呈现。基于 SALES
表,按“筛选→分组→聚合筛选→结果呈现”逻辑处理数据,以下全流程说明:
1. 底层数据(FROM 阶段)
逻辑:从 SALES
表获取原始数据,为后续筛选、聚合打基础。
-- ① 底层数据:从SALES表取原始数据
SELECT *
FROM SALES;
SALES 数据集
2. 底层筛选(WHERE 阶段)
逻辑:筛选 year = '2025'
且 category = '苹果'
的记录,缩小数据范围。
-- ② 底层筛选:在原始数据基础上按条件过滤
SELECT *
FROM SALES
WHERE year = '2025' AND category = '苹果';
筛选后的数据集
3. 聚合分组(GROUP BY 阶段)
逻辑:按 year
、month
分组,计算每组 amount
总和,聚合数据。
-- ③ 聚合分组:对筛选后数据按年月分组并求和
SELECT year, month, category, SUM(amount) AS total_amount
FROM SALES
WHERE year = '2025' AND category = '苹果'
GROUP BY year, month, category;
聚合分组后的数据集
4. 聚合筛选(HAVING 阶段)
逻辑:对分组聚合结果二次筛选,仅保留 SUM(amount) > 50000
的分组。
-- ④ 聚合筛选:对分组聚合结果再过滤
SELECT year, month, category, SUM(amount) AS total_amount
FROM SALES
WHERE year = '2025' AND category = '苹果'
GROUP BY year, month, category
HAVING SUM(amount) > 50000;
聚合分组后再筛选的数据集
5. 呈现字段(最终 SELECT 阶段)
逻辑:精简输出字段,仅保留 year
、month
、聚合金额,清晰呈现结果。
-- ⑤ 呈现字段:简化结果列,输出最终需求数据
SELECT year, month, SUM(amount) AS total_amount
FROM SALES
WHERE year = '2025' AND category = '苹果'
GROUP BY year, month
HAVING SUM(amount) > 50000;
选择字段呈现结果集
说明:上述 SQL 基于 MySQL 语法编写,若使用 Oracle、SQL Server 等数据库,需微调语法(如字符串引号、函数兼容等 ),但基本逻辑一致。
四、面试考点
问题1:为什么WHERE不能使用聚合函数?
答:聚合函数(如SUM()
)需要基于分组后的多行数据计算,而WHERE在分组前执行,此时尚无分组,无法调用聚合函数。
问题2:HAVING能否使用非聚合列?
答:只能使用GROUP BY
中指定的分组列,或聚合函数。例如:
-- 错误:非分组列且非聚合函数
SELECT department, name, COUNT(*)
FROM employees
GROUP BY department
HAVING name = '张三'; -- name未在GROUP BY中,报错
-- 正确:使用分组列
SELECT department, COUNT(*)
FROM employees
GROUP BY department
HAVING department = '技术部'; -- 分组列可直接使用
五、性能优化技巧
1. 优先使用WHERE减少数据量:
WHERE在分组前筛选,可减少参与分组和聚合的数据量,提升效率。例如:
需求“查询员工数量大于2的部门中,2020年后入职的员工数量”,应先通过WHERE筛选2020年后的员工,再分组:
-- 高效:先筛选行,再分组
SELECT department, COUNT(*) AS emp_count
FROM employees
WHERE hire_year >= 2020
GROUP BY department
HAVING COUNT(*) > 2;
-- 低效:先分组再筛选行(数据量更大)
SELECT department, COUNT(*) AS emp_count
FROM employees
GROUP BY department
HAVING COUNT(CASE WHEN hire_year >= 2020 THEN 1 END) > 2;
2. 避免在HAVING中使用复杂表达式:
聚合函数的计算成本较高,若HAVING条件复杂(如嵌套函数),会增加分组后的计算压力,建议简化条件或提前通过子查询处理。
六、避坑指南
坑1:混淆分组列与非分组列
SELECT
后若包含非分组列且非聚合函数,会导致结果不可预测(不同数据库处理逻辑不同)。例如:
-- 错误:name是非分组列且未聚合
SELECT department, name, AVG(salary)
FROM employees
GROUP BY department; -- 部分数据库报错,部分返回随机name
说明: “SELECT后若包含非分组列且非聚合函数,会导致结果不可预测”这一规则的核心是 “SELECT后的非聚合列必须全部出现在GROUP BY中”(除非数据库开启了非标准模式,如 MySQL 的ONLY_FULL_GROUP_BY关闭时)。例如:
-- 正确:SELECT的非聚合列(department)在GROUP BY中
SELECT department, AVG(salary) FROM employees GROUP BY department;
-- 错误:SELECT的非聚合列(name)不在GROUP BY中
SELECT department, name, AVG(salary) FROM employees GROUP BY department;
坑2:BETWEEN
与IN
的范围陷阱
WHERE中使用BETWEEN...AND...
时注意是闭区间(包含两端值),而IN
适用于离散值集合。例如:
-- 查询薪资在8000-12000之间的员工(包含8000和12000)
SELECT name, salary FROM employees WHERE salary BETWEEN 8000 AND 12000;
-- 查询薪资为8000、12000、15000的员工
SELECT name, salary FROM employees WHERE salary IN (8000, 12000, 15000);
坑3:HAVING中误用COUNT(*)
与COUNT(列名)
COUNT(*)
统计所有行数(包括NULL),COUNT(列名)
统计非NULL值。例如:
若某部门有1名员工薪资为NULL,COUNT(*)
会算入,COUNT(salary)
则忽略:
SELECT department, COUNT(*), COUNT(salary)
FROM employees
GROUP BY department;
七、总结
WHERE和HAVING的核心区别在于筛选时机和作用对象:
- WHERE是“前置筛选”,作用于行,用于减少原始数据量;
- HAVING是“后置筛选”,作用于分组,用于过滤聚合结果。
实际使用中,需根据是否涉及分组和聚合来选择:若需筛选单个行,用WHERE;若需筛选分组后的聚合结果,用HAVING。结合性能优化技巧和避坑指南,可写出更高效、更可靠的SQL查询。
阅读原文:原文链接
该文章在 2025/9/1 11:55:32 编辑过