1. 为什么我需要回头补基础查询这堂课
说出来有点不好意思,我离职前那个月,生产环境出了一次不大不小的故障:一个后台管理页面的列表接口,用户一点查询按钮,数据库CPU直接飙到90%以上,页面转圈十几秒才出结果。当时我第一反应是"换Redis缓存",结果架构师看了一眼SQL,丢给我一句话:"你先把基础查询搞清楚再来谈缓存。"
这句话挺扎心的,但也点醒了我。很多做了两三年的开发,写SELECT * FROM table WHERE id IN (...)写得很溜,可真要处理慢查询、去重、子查询、多表关联、分页性能这些问题时,往往靠的是网上东抄一段西抄一段,底层逻辑根本不透。等到线上出问题,只能被动的加缓存、加索引,甚至加服务器,真正的病根——SQL写法本身——反而没人管。
这篇博文我尽量把"基础查询"讲透,不局限于某一种数据库,重点覆盖SQL标准语法加上MySQL、Oracle、PostgreSQL里常用到的差异点。内容从最底层的查询逻辑开始,逐步拆解去重、子查询、临时表、删除场景下的查询陷阱、慢查询定位和分页优化,几乎每个点都对应了真实线上踩过的坑。适合刚入行的开发,也适合写了两三年SQL但没系统梳理过查询原理的朋友。
我先说明一点:这篇文章不讲ORM怎么调用,也不讲具体的连接池配置,聚焦的就是一句SQL从写出到跑完的完整链路。你跟着走一遍,把每个环节的"为什么"搞清楚,以后再遇到慢查询或者奇怪的返回结果,第一反应就不会是"换个数据库试试",而是先低头看自己写的SQL。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从SELECT执行顺序说起:WHERE和HAVING为什么会搞混
2.1 一条查询语句的真实执行顺序
很多人学SQL,上来就背语法:SELECT、FROM、WHERE、GROUP BY、HAVING、ORDER BY、LIMIT。语法没问题,但真正跑起来的时候,数据库引擎不是按这个书写顺序执行的。这个逻辑要是没理清楚,很多查询问题根本无从下手。
数据库执行一条查询的逻辑顺序是:
code复制FROM -> ON -> JOIN -> WHERE -> GROUP BY -> HAVING -> SELECT -> DISTINCT -> ORDER BY -> LIMIT
也就是说,FROM先确定数据源,WHERE先做行级过滤,GROUP BY做分组,HAVING对分组后的结果过滤,最后才轮到SELECT投影和ORDER BY排序。
举个例子,你想查"每个部门里工资大于5000的员工人数",新手经常写成:
sql复制SELECT dept_id, COUNT(*)
FROM employee
WHERE salary > 5000
GROUP BY dept_id;
这么写是对的。但如果不小心把salary > 5000放到HAVING里:
sql复制SELECT dept_id, COUNT(*)
FROM employee
GROUP BY dept_id
HAVING salary > 5000;
在MySQL里这能跑通,甚至会返回一个看似合理的结果。为什么?因为MySQL对HAVING子句里的列做了特殊容忍——它允许HAVING引用SELECT列表里的别名,也允许一些非聚合列直接出现在HAVING里。但这个行为在Oracle里直接报ORA-00979,在PostgreSQL里同样报错,因为salary既没有出现在GROUP BY里,也没有被聚合函数包裹。
这就是基础不牢的典型表现:同样的写法在MySQL里没问题,换一个数据库就挂,根子在于没有理解HAVING是"分组后的过滤条件",不是"行级过滤的备胎"。
2.2 WHERE和HAVING的边界在哪里
接着上面的例子,我把这两个子句的边界说清楚:
- WHERE:在分组之前执行,过滤的是FROM之后、GROUP BY之前的原始行。WHERE里不能使用聚合函数,比如
WHERE COUNT(*) > 2直接报错。 - HAVING:在分组之后执行,过滤的是GROUP BY产出的分组结果。HAVING可以使用聚合函数,比如
HAVING COUNT(*) > 2。 - WHERE先执行、HAVING后执行,这意味着能用WHERE过滤掉的,就别放到HAVING里。提前过滤能减少分组计算的数据量,性能差别在千万级表上非常明显。
我见过一个真实案例:有个报表SQL,要统计近30天各渠道的订单量并过滤掉下单量小于10的渠道。有人把时间条件写到了HAVING里:
sql复制SELECT channel, COUNT(*)
FROM orders
GROUP BY channel
HAVING order_time > NOW() - INTERVAL 30 DAY AND COUNT(*) >= 10;
这条SQL在MySQL里能跑,但性能很差。因为HAVING是在全表分组之后才去过滤时间,也就是说2019年以前的历史订单也被拉出来分了一轮组。把order_time条件挪到WHERE之后,同样的数据量,查询时间从4.2秒降到了0.3秒。
**我的实操建议是:HAVING里只放聚合条件,行级过滤永远用WHERE。**别问我为什么MySQL容忍这种写法,兼容性宽松不是写烂SQL的理由。
2.3 SELECT列别名在WHERE里不可用,在ORDER BY里可用
另一个容易踩的坑是别名作用域。在标准SQL里,WHERE子句的执行顺序在SELECT之前,所以WHERE里不能用SELECT中定义的别名。这个很多人知道,但等到实际写的时候还是会犯:
