在SQL语句中,筛选超过某数量的记录通常发生在分组统计之后。比如想找出下单次数超过5次的用户,或者出现次数大于1的重复邮箱,这些条件并不是针对原始行,而是针对分组后的统计结果。SQL标准为此提供了HAVING子句,它在GROUP BY之后执行,能够对每个分组做聚合条件判断。理解HAVING的执行时机,是写出正确查询的第一步。

一、HAVING与WHERE的本质区别
WHERE和HAVING都用于过滤数据,但作用阶段完全不同。WHERE在分组之前逐行过滤原始记录,它不能使用聚合函数。假如你写出WHERE COUNT(*) > 5这样的条件,数据库通常会抛出聚合函数使用无效的错误,因为此时还没有形成分组,COUNT(*)没有可统计的对象。
HAVING则是在GROUP BY之后对分组结果做筛选。它的条件里可以使用COUNT、SUM、AVG、MAX、MIN等聚合函数,也可以直接引用分组列。比如按客户ID分组后,判断每个客户的订单数量是否大于5,应该写成HAVING COUNT(*) > 5,而不是把条件塞进WHERE。逻辑上先分组、后过滤,这个顺序不可颠倒。
还需要注意,不能把普通列条件随意放到HAVING中。例如要筛选状态为已支付的订单,写在WHERE里可以更早缩小数据范围,减少后面分组的数据量;如果放到HAVING里,数据库只能先处理全部数据再过滤,执行效率会下降。简单原则是:能确定原始行条件的用WHERE,只有聚合条件才交给HAVING。
-- 错误写法:WHERE中不能使用聚合函数 SELECT customer_id, COUNT(*) AS order_count FROM orders WHERE COUNT(*) > 5 GROUP BY customer_id; -- 正确写法:聚合条件放在HAVING中 SELECT customer_id, COUNT(*) AS order_count FROM orders GROUP BY customer_id HAVING COUNT(*) > 5;
二、统计超过指定数量的标准写法
最基础的结构是:先按目标列分组,再用COUNT(*)统计每组行数,最后通过HAVING设置数量阈值。比如从商品表中找出被购买次数超过10次的商品分类,语句如下:
SELECT category_id, COUNT(*) AS total FROM products GROUP BY category_id HAVING COUNT(*) > 10;
COUNT(*)统计的是每个分组中的总行数,只要这一行存在就会计数,与具体列是否为空无关。如果用COUNT(column_name),则会忽略该列为NULL的行。例如统计用户表里重复邮箱时,COUNT(email)不会把email为NULL的行计入该分组,而COUNT(*)会。如果业务上NULL邮箱不参与重复判断,这种差异必须提前想清楚。
超过某数量往往不是唯一的过滤条件。比如要找出订单数超过5次且消费总额超过1000元的客户,可以在HAVING后面用AND组合多个聚合条件。这时候每个条件都针对分组后的统计值,语法一样直观。
SELECT customer_id, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders GROUP BY customer_id HAVING COUNT(*) > 5 AND SUM(amount) > 1000;
关于别名,MySQL允许在HAVING中直接引用SELECT里定义的别名,例如HAVING order_count > 5。但在PostgreSQL、SQL Server等数据库中,HAVING通常要求写完整的聚合表达式。为了保证SQL的可移植性,建议在HAVING中重复COUNT(*)或SUM(amount),而不要依赖别名。
三、典型场景:查找重复记录
查找重复记录是HAVING最典型的用途。假设用户表中有多个用户填了相同的邮箱,要找出这些邮箱以及出现次数,可以这样写:
SELECT email, COUNT(*) AS times FROM users GROUP BY email HAVING COUNT(*) > 1;
这个查询会按email分组,每组包含所有该邮箱对应的用户行。HAVING COUNT(*) > 1表示只保留行数超过1的邮箱,也就是重复邮箱。如果想查看具体是哪些用户,可以再把这个结果作为子查询,关联用户表取出完整信息。
多列联合重复也很常见。比如订单明细表中,同一个用户对同一个产品购买了多次,可以按user_id和product_id两个列分组。只有当两列组合相同的行数超过1时,才认为存在重复购买。
SELECT user_id, product_id, COUNT(*) AS buy_times FROM order_items GROUP BY user_id, product_id HAVING COUNT(*) > 1 ORDER BY buy_times DESC;
ORDER BY可以放在HAVING之后,对过滤后的分组结果排序。这样能快速看到重复次数最高的组合。不同数据库限制分页的写法略有差异,MySQL用LIMIT,SQL Server用TOP或OFFSET FETCH,但HAVING部分完全一致。
四、常见坑与性能优化建议
NULL值是一个容易忽略的坑。如果分组列中存在NULL,所有NULL会归入同一个分组。考虑COUNT(*)和COUNT(column)的区别:COUNT(*)会把NULL行也统计进去,COUNT(column)则不会。比如查重复邮箱时,如果存在大量NULL邮箱,使用COUNT(email)会得到0,不会把它们识别为重复,但COUNT(*)会把所有NULL行归为一组并显示数量。业务上是否要清理NULL,需要在分组前用WHERE email IS NOT NULL明确处理。
SELECT email, COUNT(*) AS times FROM users WHERE email IS NOT NULL GROUP BY email HAVING COUNT(*) > 1;
性能方面,HAVING作用在聚合之后,通常无法直接使用原始表上的普通索引。如果数据量很大,应尽量在WHERE中先过滤掉无关数据,减少GROUP BY需要处理的行数。例如统计最近30天订单数超过5次的用户,先把时间范围写进WHERE,比把所有历史订单聚合后再筛选要快得多。
SELECT customer_id, COUNT(*) AS order_count FROM orders WHERE order_date >= CURRENT_DATE - INTERVAL '30 days' GROUP BY customer_id HAVING COUNT(*) > 5;
当聚合结果还要被重复引用时,可以借助CTE或子查询把分组统计结果先物化。这样可以避免在多个地方重复写复杂的HAVING表达式,也能让执行计划更清晰。窗口函数ROW_NUMBER()也能实现部分类似功能,但它更适用于需要保留明细行的排名场景;如果只关心分组统计值,HAVING更加简单直接。不同数据库对CTE的支持已经比较普遍,比如WITH grouped AS用来存放聚合结果,再在外层筛选想要的阈值。
WITH grouped AS (
SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
)
SELECT customer_id, order_count
FROM grouped
WHERE order_count > 5;
总结来说,查询超过某数量的记录,核心是把聚合条件放在HAVING中,并理解它和WHERE、GROUP BY的先后关系。根据业务选择COUNT(*)还是COUNT(column),提前处理NULL,尽量先WHERE缩小范围,就可以在保证结果准确的同时获得更好的查询性能。
SQL HAVING分组过滤COUNT修改时间:2026-10-04 10:48:07