做后端开发和数据分析这几年,我反复看到同一种现象:不少人写过不少SQL,但只要业务条件一复杂,就开始一层套一层子查询、随手LEFT JOIN、到处丢DISTINCT,最后结果对不对全看运气。问题多数时候不是语法忘了,而是没弄清楚SQL的本质是集合操作,不是逐行计算。SQL查询这门手艺,最核心的就是两件事——筛选和联结:先用一套干净的基础筛选规则把数据范围逐步收紧,再通过多表联结把分散的业务信息重新组合成一张能支撑决策的分析宽表。这篇文章就不绕弯子,从WHERE基本功讲到JOIN、聚合,再把窗口函数这个真正的进阶利器打开看一遍,希望能帮你建立一条完整的学习主线。
如果你是刚接触SQL的人,这篇文章能让你少走弯路;如果你已经会写不少查询但总被慢SQL、结果对不上折磨,里面讲的优化思路和常见误区应该也能对得上号。
1. 为什么很多人的SQL是“拼”出来的:集合思维才是查询的内功
老话说“思路决定出路”,在SQL上体现得特别明显。我见过太多人把SQL当Java、Python写:遇到需求,第一反应是“先查出这个,再循环那个,再根据条件判断”,于是在一条查询里塞子查询、塞CASE WHEN、塞各种关联,把简单问题硬做成了“俄罗斯套娃”。这种写法不能说完全错,但往往既慢又难维护,而且一旦业务逻辑稍微变一下,整段SQL就废了。
1.1 从过程式到声明式:SQL想问题的姿势完全不同
很多人没意识到,SQL和主流编程语言是两种完全不同的思维模式。
过程式语言的思路是“我一步一步做”:先打开文件、循环每一行、判断条件、累加变量、输出结果。SQL的思路是“我告诉你最终想要的集合长什么样”:给我2024年注册且有下单的用户,系统自己决定怎么去扫描、关联、排序。
用一句话概括:SQL是声明式的,不是过程式的。你声明“要哪些数据”,而不是“怎么拿这些数据”。
举个最典型的例子,需求是“找出2024年注册并且下过订单的用户名”,新手可能会写出这种:
sql复制SELECT DISTINCT u.name
FROM users u
WHERE u.id IN (
SELECT o.user_id
FROM orders o
WHERE o.user_id IN (
SELECT id FROM users WHERE registered_at >= '2024-01-01'
)
);
SQL本身没问题,能跑出结果,但读起来很费劲。稍微有点经验的人一眼就能看出,子查询里的 inner SELECT 完全可以直接放到外层JOIN之前做过滤。用集合思维的话,你首先会想:最终结果是一个“用户名集合”,这个集合来自“用户表”和“订单表”的交集,同时用户表还要满足注册时间条件。于是写法变成:
sql复制SELECT DISTINCT u.name
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.registered_at >= '2024-01-01';
两条语句的结果一样,但第二条明显更贴近业务语义:我需要的是一个集合,而不是一段“操作流程”。数据库优化器拿到第二条语句时反而更容易选择最优执行路径,因为它的语义更干净,没有多余嵌套。
1.2 当你“拼接”SQL时,数据库优化器也在遭罪
还有一个反向的问题:有些人写SQL时特别爱“炫技”,一个查询里堆五个子查询、两个CTE、三个窗口函数,仿佛写得越复杂越专业。SQL写出来不算完,数据库还得把它翻译成执行计划。你写得越绕,优化器能做的等价变换空间就越小。
我经常跟团队讲:不要以为自己写得长就是考虑得周全。SQL的艺术恰恰是能用最简单的语义表达出完整的需求,就绝不用多余结构。这条原则贯穿筛选、联结和分组统计的每一个环节。
不信你可以去EXPLAIN一下那种嵌套五层的查询,再看一眼同样需求用JOIN + WHERE实现的执行计划,很多情况下后者的预估行数和扫描方式会明显更优。这就是集合思维的实用价值,不止是好读、好维护,它还能直接影响查询性能。所以你问我学SQL第一课学什么,我一定会说:不是学某个函数,而是把你的思维方式从“逐行处理”切换成“集合变换”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. WHERE基本功:筛选条件怎么写,查询才不容易翻车
选好集合之后,第一个真正动手的操作就是筛选。别小看WHERE,这个子句的写法决定了你后面所有操作的准确性。我见过太多线上Bug,根因都是WHERE的一句话写歪了。
2.1 先记住逻辑执行顺序,很多问题迎刃而解
很多教材一上来就让你背SQL各子句的书写顺序:SELECT、FROM、WHERE、GROUP BY、HAVING、ORDER BY、LIMIT。这个当然要记,但更重要的是逻辑执行顺序:
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT
注意,WHERE是在分组之前执行的,HAVING是在分组之后执行的。这个顺序看起来不起眼,实际上决定了你能不能理解“为什么WHERE里不能直接用聚合函数”,也决定了“为什么HAVING能过滤组,WHERE不能”。
一条实用的经验:能下推到WHERE的筛选条件,绝不要留到HAVING。因为WHERE提前过滤掉大量行,后面分组、排序、联结的负担都会小很多。很多慢查询的根源,就是把本可以在WHERE做掉的过滤写到了外层或HAVING里,导致数据库处理了大量无关数据。
2.2 NULL、BETWEEN和函数包裹:筛选条件里的三个大坑
第一坑:NULL。
SQL里的NULL不是0,也不是空字符串,它代表“未知”。所以判断一个字段是否为NULL,绝对不能写成 字段 = NULL,因为NULL = NULL的结果不是true,而是unknown。你需要用 IS NULL 或 IS NOT NULL。
sql复制-- 错误写法:查不出任何结果
SELECT * FROM users WHERE deleted_at = NULL;
-- 正确写法
SELECT * FROM users WHERE deleted_at IS NULL;
很多人刚学的时候都在这里栽过跟头,因为WHERE条件里一旦混入NULL判断,结果集会凭空少掉一批行,而且很难察觉。
第二坑:BETWEEN AND的边界。
BETWEEN a AND b 在SQL标准里是闭区间,也就是包含a和b本身
