SQL中where和having的区别:执行顺序、聚合函数与性能实战解析
写SQL也写了这么多年还是经常在技术群里看到有人问where和having到底啥区别。说实话这个问题几乎每个刚接触SQL的人都会绕进去甚至不少工作了几年的人写复杂统计报表时也会卡在这两个关键字的用法上。我印象最深的一次是帮同事排查一个订单汇总数据的错误他非要在where里写sum(amount) 1000结果SQL直接报错改成having之后又发现数据结果还是不对——因为他压根没搞明白执行顺序把分组前的行过滤和分组后的组过滤混在一起。这篇文章不绕弯子直接把这个知识点给你掰扯清楚。我会从底层执行顺序讲起结合完整的建表数据和实战SQL把where和having在过滤时机、能否使用聚合函数、与group by的关系、性能影响这几个维度全部对比一遍。如果你是SQL新手或者写过一段时间SQL但对这两个关键字一直模棱两可这篇文章就是给你准备的。1. 先搞懂一条SQL是怎么“跑”完的1.1 执行顺序决定一切很多人学SQL是从select开始背的select * from 表 where 条件久而久之就形成了一种错觉SQL是从select开始执行的。这是一个特别危险的认知因为where和having的区别根源就在执行顺序上。一条带有分组、过滤的完整SQL逻辑执行顺序是这样的-- 原始SQL select 部门, sum(金额) as 总额 from 销售表 where 销售日期 2024-01-01 group by 部门 having sum(金额) 10000 order by 总额 desc;上面这条SQL在数据库内部真实执行顺序是from先定位到销售表where对整个表进行逐行扫描把销售日期满足条件的行筛选出来group by把筛选后的结果按部门分组having对分组后的结果进行组级过滤把总额不满足条件的组去掉select对最终留下的组进行计算和投影order by最后排序这里最关键的两步where在分组之前执行having在分组之后执行。你可以把where理解成进入仓库前的安检——身份不符的人直接拦在门外把having理解成装车完之后的称重——超重的整批货物都得卸下来。一个过滤的是行一个过滤的是组工作发生的阶段完全不同。1.2 为什么这个顺序会直接导致报错搞清了执行顺序很多报错现象就都能解释了。比如你写select 部门, sum(金额) from 销售表 where sum(金额) 10000 group by 部门;这个SQL在绝大多数数据库里都会直接报错报错信息一般是无效使用聚合函数或者聚合函数不应出现在WHERE子句中。原因很简单where执行的时候分组还没发生每一行都是独立的这时候压根不存在汇总后的金额自然没法拿sum()的结果去做比较。就像你还没把苹果装进筐里就问哪筐苹果超过10斤——这是逻辑顺序上的错误。同样的道理如果你想过滤的是2024年1月1日之后的记录把它写在having里也不是不行但效率很低。因为where先过滤掉大量无关行参与分组的数据量就小如果非要where能干的事让having干所有原始行都得先分组完再一条条判断数据和性能都会出问题。这个后面细说。2. having和where的五大区别直接对照记2.1 过滤时机不同分组前 vs 分组后这是最本质的区别前面已经提到了。用一句口诀记忆where先过滤行having后过滤组。打个比方你要统计班级里各小组的平均分并且只看平均分超过80分的小组。where负责的是只统计成绩大于0的有效试卷这种行级操作它处理的是每条考生记录having负责的是平均分达到80的小组才进入结果它处理的是每个小组的统计结果。2.2 能否使用聚合函数一条硬边界这是最实用、最常被拿来判断的一条标准where不能使用聚合函数sum、avg、count、max、min等having可以使用聚合函数你可以在where后面写where 金额 100但不能写where sum(金额) 100。反过来having后面既可以写having sum(金额) 100也可以写having 金额 100——当然having里写非聚合条件不是好习惯因为它是组级别过滤如果条件不涉及聚合结果放在where里更高效。2.3 与group by的关系依赖程度不同where完全不依赖group by一条最简单的select * from 表 where 条件就可以单独存在。having和group by的关系则更紧密。虽然从语法上说MySQL允许你在没有group by的情况下使用having这时它会把整个结果集当成一个隐式的组但这条语法在业务逻辑上容易把人绕晕而且和标准的SQL规范不完全一致。在SQL Server、Oracle这些数据库里没有group by直接使用having经常报错。更规范的理解是having是专为分组结果设计的过滤工具通常跟在group by后面使用。2.4 可使用的字段范围不同where后面可以写表中的任意原始列也可以使用各种非聚合表达式比如where 金额 * 数量 100。having后面则更多写聚合表达式或者出现在group by里的分组列。比如having avg(金额) 100这个avg(金额)在where里就写不了。有个容易踩坑的点是having里能不能用select中定义的别名这个不同数据库行为不一样MySQL可以SQL Server就不太行。后面我会专门讲这个坑。2.5 性能差异能走索引 vs 躲不过全组计算where条件如果恰好命中索引列数据库可以直接通过索引快速定位到符合条件的行效率极高。where是在原始数据准备阶段就缩小了数据规模越早过滤后续分组、排序、计算的开销越小。having里的条件无法通过索引去优化因为它的输入是分组后的计算结果这些结果往往是实时算出来的。尤其是当原始表数据量很大的时候如果你的having条件能写成where条件却把它写在having里等于让数据库先把所有数据分组聚合一遍再把不合适的组扔掉。该省的活一点没省白白增加计算压力。所以性能上的黄金法则就是能下放到where的过滤条件千万不要放在having里。我整理了一张汇总对比表方便你复习时快速抓重点对比项wherehaving执行阶段group by之前group by之后过滤对象原始行记录分组后的组是否可用聚合函数不可用可用是否必须配合group by不需要通常需要MySQL无group by时也可但不推荐性能特征能利用索引早过滤无法走索引基于计算结果过滤与select别名的关系基本不能使用别名MySQL中可以使用select别名典型场景筛选原始明细数据筛选聚合统计结果3. 实战演示用一份销售数据把两个关键字跑透3.1 造一张测试表这一节的内容比较重要建议你打开数据库跟着敲一遍。我用一套比较通用的建表语句MySQL、SQL Server、PostgreSQL基本都能跑只是自增列的写法略微不同。我以MySQL为例create table sales ( id int primary key auto_increment, salesperson varchar(20) comment 销售员, region varchar(20) comment 区域, product varchar(30) comment 产品, sales_amount decimal(10, 2) comment 销售金额, sale_date date comment 销售日期 );插入几条测试数据insert into sales (salesperson, region, product, sales_amount, sale_date) values (张伟, 华东, 手机, 5000.00, 2024-01-05), (张伟, 华东, 电脑, 8000.00, 2024-01-12), (李娜, 华北, 手机, 3000.00, 2024-01-08), (李娜, 华北, 平板, 2000.00, 2024-01-15), (王强, 华南, 电脑, 12000.00, 2024-02-01), (王强, 华南, 手机, 4000.00, 2024-02-10), (赵敏, 华北, 电脑, 9000.00, 2024-02-15), (张伟, 华东, 平板, 1500.00, 2024-03-01), (李娜, 华北, 电脑, 11000.00, 2024-03-10), (王强, 华南, 平板, 2500.00, 2024-03-12);造这些数据有一个故意的设计每个销售员都有多笔销售记录有金额高的也有金额低的这样方便我们同时演示行级过滤和组级过滤的差异。3.2 场景一纯行级过滤用where就对了需求查询华东区域的销售记录。select * from sales where region 华东;这个需求只涉及原始行的筛选跟分组、聚合完全没关系用where天经地义。如果你脑子一热写成select * from sales having region 华东;这个SQL在MySQL里可能不会立即报错但返回的结果是整张表的所有记录因为having在没有group by的时候把整张表当成一个组而region 华东在这个组里压根不是有效条件——它不会像where那样逐行去判断。在SQL Server里这条SQL直接报错。这是一个典型的写法看着差不多语义差很远的例子。3.3 场景二先按人分组再过滤组级别数据用having需求查询总销售金额大于10000的销售员。这个需求必须先对销售员分组然后计算每个人的总金额最后筛掉总额不超过10000的人。select salesperson, sum(sales_amount) as 总金额 from sales group by salesperson having sum(sales_amount) 10000;执行结果salesperson总金额张伟14500.00李娜16000.00王强18500.00赵敏的总额只有9000没有被选进来。这里的核心逻辑是sum(sales_amount)必须等group by salesperson分组完成后才能计算所以这个条件只能放在group by之后的having里。3.4 场景三where和having同时出现各干各的活需求查询2024年2月1日之后各组销售总额大于8000的销售员。这个需求同时包含行级过滤和组级过滤select salesperson, sum(sales_amount) as 总金额 from sales where sale_date 2024-02-01 group by salesperson having sum(sales_amount) 8000;执行逻辑是先把sale_date 2024-02-01的7条记录选出来再按销售员分组最后筛选总额大于8000的组。你对比一下如果把日期条件挪到having里select salesperson, sum(sales_amount) as 总金额 from sales group by salesperson having sum(sales_amount) 8000 and sale_date 2024-02-01;这条语句在MySQL里会产生一个容易让人困惑的结果在某些模式设置下甚至报错因为sale_date没有出现在group by中也没有被聚合函数包裹。就算能跑语义也已经变了它不再是对原始行先过滤再分组而是对全部分组结果强行附带一个行级条件逻辑上就是错的。实操心得当你发现一条SQL又要过滤明细、又要过滤分组结果养成一个习惯——先写where把明细过滤干净再写having做组级筛选。两条过滤条件各管各的阶段逻辑一目了然性能也更好。3.5 场景四被很多人误解的没有group by的having有一种写法特别容易出现在非专业代码里select sum(sales_amount) as 总金额 from sales having sum(sales_amount) 50000;这条SQL在MySQL中是可以运行的整个表被当成一个隐含组最后返回一个汇总值。但在SQL Server中这条SQL会报错。而且从规范的SQL标准角度这种写法也不推荐因为它的行为在不同数据库中差异太大。我的建议是如果你确实需要对整个表做聚合后再过滤写成子查询或使用其他明确的写法避免依赖这种隐式分组行为。比如select 总金额 from ( select sum(sales_amount) as 总金额 from sales ) t where 总金额 50000;这样语义清晰跨数据库兼容性也更好。4. 进阶细节那些让人抓狂的边界情况4.1 whether having里能用select别名先看数据库场景是这样的你写select salesperson, sum(sales_amount) as 总金额 from sales group by salesperson having 总金额 10000;在MySQL里这个写法是可以运行的因为MySQL对having子句中的列名解析扩展到了select别名。但在SQL Server、Oracle等数据库中这条SQL会报错比如SQL Server会提示列名总金额无效。因为逻辑执行顺序里select是在having之后才执行的别名在having阶段还不存在。实操心得为了让SQL在不同数据库之间都能跑最稳妥的写法是不要在having里用别名老老实实写完整表达式select salesperson, sum(sales_amount) as 总金额 from sales group by salesperson having sum(sales_amount) 10000;虽然多敲了几个字符但是兼容性最好别人读代码时也能直接看到聚合逻辑。4.2 用过group by后select里到底能放哪些列这个问题和having/where的关系很大因为很多人的报错根源不在having或where本身而在group by和select列的组合上。标准SQL规定使用了group by之后select里只能放分组列和聚合函数。比如-- 正确 select region, sum(sales_amount) from sales group by region; -- 错误示范salesperson既不在group by里也没被聚合函数包裹 select region, salesperson, sum(sales_amount) from sales group by region;第二条SQL在SQL Server、Oracle里绝对报错MySQL虽然默认配置下能跑因为它有ONLY_FULL_GROUP_BY这个模式开关默认关着但这里藏着隐患——如果同一个region下有多个salespersonMySQL会返回随机的一条数据是错的。所以遇到having相关的报错第一步不是检查having而是回头看group by和select的列是否匹配。很多时候是select里放了不该放的列导致整个查询语义错乱having只是背锅的那个。4.3 having 1 1这种写法是干嘛的有些老项目里能看到这类SQLselect region, sum(sales_amount) from sales group by region having 1 1;这个写法常常是代码动态拼接SQL时留下的占位条件目的是让后续所有追加的条件都能用and连起来避免每次都要判断前面是否已经有where或having。说实话这种写法虽然能跑我自己不太推荐在生产环境里使用因为它会干扰代码阅读而且11这种常量条件会导致数据库优化器白白多判断一次虽然性能影响微乎其微但属于可以避免的脏代码。如果非要用占位条件更推荐在代码层面统一处理拼接逻辑。4.4 where和having都能实现金额大于时写哪里效率高还是拿前面的销售表做例子。需求是查询销售金额大于4000的单笔销售记录并按销售员分组看每个销售员的销售次数。你可以写成-- 写法一先where过滤再分组 select salesperson, count(*) as 次数 from sales where sales_amount 4000 group by salesperson;也可以写成-- 写法二先分组再having过滤 select salesperson, count(*) as 次数 from sales group by salesperson having min(sales_amount) 4000;注意写法二语义已经变了它过滤的是组内最小金额都大于4000的销售员而不是单笔大于4000的记录。所以如果需求是满足条件的记录次数第一种写法才是对的。但假设需求恰好是组内经筛选后的计数大于某个数比如统计每个销售员单笔金额大于4000的记录数并且只看次数大于等于2的销售员就可以组合select salesperson, count(*) as 次数 from sales where sales_amount 4000 group by salesperson having count(*) 2;这里where负责砍掉不满足条件的单笔记录having负责把计数不达标的销售员去掉各司其职。5. 常见报错与排查技巧5.1 无效列名类报错先查三件事不管是SQL Server还是其他数据库只要看到无效列名或者ambiguous column这类报错建议按下面的顺序排查列名是不是真的存在于当前表里列名是不是被写错成了select里的别名列名的位置是不是放错了比如把原始列放在having里而它又没有出现在group by中绝大多数having相关的报错都能归到第三类。比如你在having里写了一个既不是聚合函数、又没出现在group by中的原始列数据库会直接拒绝解析因为它不知道如何按这个原始列去过滤一个组。5.2 聚合函数进where报错聚合函数不应出现在此处这条不用怀疑100%是写法问题。比如select region from sales where sum(sales_amount) 10000 group by region;看到这个报错直接把sum(sales_amount) 10000挪到having里就行。如果还需要对原始行做限制把原始行的限制留在where里两件事不要混在一块。5.3 同一个条件放where和放having结果不一样为什么举一个我实际在项目中见过的问题。有人写-- 需求查2024年1月之后下单且总金额大于10000的客户 -- 错误写法 select customer_id, sum(amount) from orders group by customer_id having sum(amount) 10000 and order_date 2024-01-01;这个写法的问题在于order_date既不在group by里也不是聚合函数。数据库中这条SQL要么报错要么MySQL产生一个错误的随机结果——某个customer_id下随机一条order_date参与了判断。正确写法是select customer_id, sum(amount) from orders where order_date 2024-01-01 group by customer_id having sum(amount) 10000;记住一个原则凡是对原始行的筛选不管它是不是跟聚合结果一起出现在需求里永远先放where。5.4 碰到大表性能问题怎么从where/having上找突破口慢SQL是绕不开的话题。我在优化报表SQL时最先看的就是where条件有没有落在索引列上以及过滤条件下放得够不够早。有次帮运营改一条数据量在千万级的订单表统计SQL原本的写法是select user_id, sum(pay_amount) from orders group by user_id having sum(pay_amount) 500 and max(order_date) 2024-01-01;这条SQL慢得离谱因为所有千万级的数据都参与了分组聚合然后才做一次组级过滤。我把对order_date的限制下放到whereselect user_id, sum(pay_amount) from orders where order_date 2024-01-01 group by user_id having sum(pay_amount) 500;order_date如果刚好有索引数据库只取今年以来的数据参与聚合成百上千倍的减少查询时间从几十秒降到了几百毫秒量级。这就是我在前面反复强调的那句话能下放到where的过滤条件千万不要放在having里。这不是风格喜好问题是实打实的性能差距。5.5 不同数据库的特殊行为一张表说清楚场景MySQLSQL ServerOraclePostgreSQLhaving里用select别名支持不支持不支持支持11及以上部分场景无group by直接having支持隐式分组不支持支持但语义值得商榷支持select非聚合列不在group by中默认允许关掉ONLY_FULL_GROUP_BY时不允许不允许不允许语义上不推荐聚合函数放在where报错报错报错报错这张表是我在多个测试环境里踩出来的给你做个参考。实际开发时建议还是以标准SQL为准不要依赖某个数据库的宽松行为不然哪天换了数据库代码稀里哗啦全红。注意如果你在公司用的是老版本的MySQL5.7以下ONLY_FULL_GROUP_BY默认没有开启很多不规范SQL能跑通但结果可能是错的。这一点在从MySQL迁移到其他数据库时尤其致命迁移前建议把所有group by查询都按标准SQL过一遍。6. 写过滤条件时我给新手的几条实在建议第一先写where再写having。拿到一个过滤需求第一反应先判断这是对原始明细行的过滤还是对分组结果的过滤前者放where后者放having。如果两个都有先写where再写having。第二不要贪图省事在where里写聚合函数。有些初学者想用一个条件完成两件事直接在where里写sum(...) 100报错后才开始改。一句口诀where里的行是单独的having里的行是成组的想用聚合只能在成组之后。第三having里的条件尽量只写跟组有关的条件。如果一个条件拿到where里能跑那就放where里。在LIKE、等值判断、范围判断这类普通条件上where永远比having高效而且语义更清晰。第四写完SQL后在脑子里面过一遍执行顺序。这是一劳永逸的办法。不管SQL多长都按from-join-where-group by-having-select-order by这个顺序过一遍很多莫名其妙的错一眼就能看出来。第五多写多练故意试错。我学习where和having时曾经故意写错SQL观察报错信息和结果比如把sum条件放where里看它怎么报错把原始列放having里看它怎么解析。你亲手踩过一个坑比看十篇文章都记得牢。我个人在实际项目中的体会是having和where的区别不只在语法层面更体现在思考方式上。你写where时脑子里是一张平铺的明细表写having时要自动切换到我已经完成了分组现在面对的是一个个小组的心理模型。这个切换一旦养成习惯再看统计类SQL思路清晰得不是一星半点。希望这篇文章能把你从两个关键字傻傻分不清的状态里彻底拉出来。有什么拿不准的写法欢迎按文中的方式自己造数据验证——毕竟SQL这东西亲手跑一遍比记忆任何规则都靠谱。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →