三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

零基础学SQL 03:ORDER BY / GROUP BY / HAVING——查询收尾三件套,别再写错顺序

零基础学SQL 03:ORDER BY / GROUP BY / HAVING——查询收尾三件套,别再写错顺序

先说点真话

当年做数据分析项目,有回老板要"按部门算平均工资,只保留平均超过 9100 的部门"。新人交上来的 SQL 八成报错——要么把筛平均工资的判断写成了WHERE AVG(工资) > 9100(直接语法报错),要么WHEREGROUP BY顺序乱写。

其实就差一件事没搞懂:排序、分组、分组后过滤,这三块各自在哪个阶段生效。今天把它们一次讲透,你以后再写这类报表就不慌了。

💡 「SQL 从入门到精通」系列持续更新中,关注我不迷路,每篇都是实战干货。


一、ORDER BY:查完之后,给结果排个序

你会什么场景下用到它?
SELECT把数据查出来了,但想按"工资从高到低"看谁排前面、谁垫底,或者按"入职日期从早到晚"看资历。这时候就要在查询最后加ORDER BY

正确写法:

SELECT姓名,工资FROM员工表ORDERBY工资DESC;-- DESC 从高到低;ASC 从低到高(默认)

结果(按工资降序):

姓名工资
王五9200
赵六9200
张三9000
李四9000

几个要点:

  • 可以多列排序:ORDER BY 部门 ASC, 工资 DESC(先按部门,同部门内再按工资)。
  • ORDER BY永远写在查询最后(在WHEREGROUP BYHAVING之后)。

我当年踩的坑:想按"算出来的平均工资"排序,写成ORDER BY 平均SELECT里给列起了别名,有的数据库认别名、有的不认,最稳的是直接写ORDER BY AVG(工资)


二、GROUP BY:按维度归类汇总(必须搭配聚合函数)

你会什么场景下用到它?
你想"按部门统计人数、算平均工资",而不是一行行看。这种"先分组、再对每组算一个数"的需求,GROUP BY就是干这个的。

但这里有个前提:GROUP BY自己不会算数,它必须配合聚合函数——COUNT()(计数)、SUM()(求和)、AVG()(平均)、MAX()(最大)、MIN()(最小)。

正确写法:

SELECT部门,AVG(工资)AS平均工资,COUNT(*)AS人数FROM员工表GROUPBY部门;

结果:

部门平均工资人数
技术部9000.002
销售部9200.002

最大的坑(新手必踩):
SELECT里出现的非聚合列,必须同时出现在GROUP BY后面,否则直接报错。比如下面这种就会报错:

-- ❌ 错误:姓名没进 GROUP BY,数据库不知道"每个部门的姓名该显示哪一行"SELECT部门,姓名,AVG(工资)FROM员工表GROUPBY部门;

记住口诀:分组查什么维度,SELECTGROUP BY就列什么维度,其余一律包进聚合函数。


三、HAVING:分组"之后"再过滤(和 WHERE 不是一回事)

你会什么场景下用到它?
接上面的例子:你按部门算完平均工资了,但只想看"平均超过 9100 的部门"。这个"平均>9100"是分组之后才产生的数,不能用WHERE去筛——因为WHERE是在分组之前过滤原始行的,它根本不知道"平均工资"是什么。

正确写法:

SELECT部门,AVG(工资)AS平均工资FROM员工表GROUPBY部门HAVINGAVG(工资)>9100;-- 分组之后,再筛分组结果

结果(只有销售部 9200 达标):

部门平均工资
销售部9200.00

四、WHERE vs HAVING,一图分清(重点)

这是新手最容易混的一对,记住这个类比就通了:

WHERE像"进考场前查身份证"——在分组/汇总之前过滤原始行,不能用聚合函数。
HAVING像"考完试按分数筛人"——在分组/汇总之后过滤分组结果,能用聚合函数。

对比项WHEREHAVING
生效阶段分组分组
能不能用 AVG()/SUM() 等❌ 不能✅ 能
典型用途过滤某行(如工资>8000)过滤某组(如平均>9100)

错误示范(别这么写):

-- ❌ 报错:WHERE 里不能用聚合函数SELECT部门,AVG(工资)FROM员工表WHEREAVG(工资)>9100GROUPBY部门;-- ✅ 正确:筛分组结果用 HAVINGSELECT部门,AVG(工资)FROM员工表GROUPBY部门HAVINGAVG(工资)>9100;

两者还能一起用:比如"只看技术部,且技术部平均>9100"——WHERE 部门='技术部'(分组前先砍掉其他部门)+HAVING AVG(工资)>9100(分组后再筛)。


五、当年我踩过的 3 个坑(重点看)

  1. 把 HAVING 的条件写成 WHERE
    想筛"平均>9100 的部门",手滑写成WHERE AVG(工资)>9100,直接报语法错。结论:对"分组后算出来的值"做判断,一律用 HAVING。

  2. SELECT 里的非聚合列没进 GROUP BY
    写了SELECT 部门, 姓名, COUNT(*),却只GROUP BY 部门,数据库不知道姓名取哪行,报错。结论:分组维度要前后一致。

  3. ORDER BY 放错位置
    写在GROUP BY前面,报"列无效"。结论:逻辑执行顺序永远是FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY

    注意:这是"引擎心里跑的顺序",不是你"写代码的顺序"。你写 SQL 时把SELECT放第一句只是语法规定(方便人读),引擎其实最后才执行它——SELECT这一步干的是"投影":等前面的行和组都定了,才决定每一行输出哪些列、算哪些表达式、起什么别名。也正因为SELECTWHERE之后,你不能在WHERE里用SELECT里起的别名(比如WHERE 年薪>10万年薪SELECT里算的,会报错),这正好证明WHERESELECT先跑。


六、3 个练习(自己跑一遍)

基于下面的员工表

  1. 按工资从低到高排,查出所有人的姓名和工资。
  2. 按部门统计每个部门的最高工资。
  3. 订单表,按员工id分组,查出"订单金额合计超过 5000"的员工及其合计金额。SQL:GROUP BY 员工id+HAVING SUM(订单金额) > 5000。(思考题:如果还想在结果里显示员工姓名,要不要 JOIN员工表`?为什么只查订单表也够用?)

参考答案(先自己写,再对照):

-- 1SELECT姓名,工资FROM员工表ORDERBY工资ASC;-- 2SELECT部门,MAX(工资)AS最高工资FROM员工表GROUPBY部门;-- 3(只用到订单表即可:员工id 与 订单金额 都在订单表里,无需 JOIN;若想显示姓名才需再 JOIN 员工表)SELECTo.员工id,SUM(o.订单金额)AS合计FROM订单表 oGROUPBYo.员工idHAVINGSUM(o.订单金额)>5000;

七、小结

  • ORDER BY:查完排序,写在最后,多列用逗号。
  • GROUP BY:按维度汇总,必须配合聚合函数,SELECT非聚合列要进GROUP BY
  • WHERE管"分组前",HAVING管"分组后",对平均值/总和做判断用HAVING
  • 执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY

下一篇我们讲条件过滤进阶:AND / OR / IN / BETWEEN / LIKE,把WHERE里的组合拳打全。


互动时间(站队投票):你写 GROUP BY 时最容易忘的是哪块?我当年铁定选"把 HAVING 的条件写成 WHERE,然后对着报错发呆半小时"😂。是 HAVING 的位置、还是 SELECT 非聚合列要进 GROUP BY、还是 ORDER BY 顺序?评论区打 1/2/3,我挑典型的下篇专门讲。

👋 我是 Hello_world12131,十年数据分析经验。
这篇有用的话点个赞 + 收藏,顺便关注我📌
📎 系列目录(置顶) |
🔗 相关推荐:
篇02 SELECT/FROM/WHERE ·
SQL 常用函数速查表 ·
SQL 多表关联查询入门 |
⏭️ 下篇预告:篇04 条件过滤 AND/OR/IN/BETWEEN/LIKE


📋 SQL 系列统一练习表(复制即可运行,可重复执行,不会报"表已存在")

本系列所有文章都基于下面这 4 张固定表。每张表先DROPCREATE,所以每读一篇文重新跑一次这个块都安全,不用从新建表。

DROPTABLEIFEXISTS员工表;CREATETABLE员工表(员工idINTPRIMARYKEY,姓名VARCHAR(20),部门VARCHAR(20),工资DECIMAL(10,2),邮箱VARCHAR(50),手机VARCHAR(20),入职日期DATE);INSERTINTO员工表(员工id,姓名,部门,工资,邮箱,手机,入职日期)VALUES(1,'张三','技术部',9000.00,'zhangsan@demo.com','13800000001','2019-03-01'),(2,'李四','技术部',9000.00,'lisi@demo.com','13800000002','2020-07-15'),(3,'王五','销售部',9200.00,'wangwu@demo.com','13800000003','2018-01-10'),(4,'赵六','销售部',9200.00,'zhaoliu@demo.com','13800000004','2021-05-20');DROPTABLEIFEXISTS订单表;CREATETABLE订单表(订单idINTPRIMARYKEY,员工idINT,订单金额DECIMAL(10,2),下单时间DATETIME,付款时间DATETIME,状态VARCHAR(20));INSERTINTO订单表(订单id,员工id,订单金额,下单时间,付款时间,状态)VALUES(101,1,3000.00,'2026-01-10 10:00:00','2026-01-10 10:05:00','已付款'),(102,1,2500.00,'2026-02-15 14:00:00','2026-02-15 14:10:00','已付款'),(103,3,4000.00,'2026-03-20 09:30:00','2026-03-20 09:40:00','已付款'),(104,2,1500.00,'2026-04-05 16:00:00',NULL,'待付款');DROPTABLEIFEXISTS用户表;CREATETABLE用户表(用户idINTPRIMARYKEY,姓名VARCHAR(20),手机VARCHAR(20),邮箱VARCHAR(50),地址VARCHAR(100));INSERTINTO用户表(用户id,姓名,手机,邮箱,地址)VALUES(1,'张三','13900000001','zhangsan@demo.com','北京市朝阳区'),(2,'李四','13900000002','lisi@demo.com','上海市浦东新区'),(3,'王五','13900000003','wangwu@demo.com','广州市天河区');DROPTABLEIFEXISTS任务表;CREATETABLE任务表(任务idINTPRIMARYKEY,员工idINT,备注VARCHAR(100),状态VARCHAR(20));INSERTINTO任务表(任务id,员工id,备注,状态)VALUES(1,1,'完成需求评审','已完成'),(2,2,NULL,'进行中'),(3,3,'修复线上bug','已完成'),(4,4,NULL,'待分配');
← 返回列表