☰
SQL优先级与执行顺序:一次讲清AND、JOIN、WHERE的坑
2026/10/17 12:40:31 网站建设 项目流程

先说一个我印象很深的场景。某天一位同事发了条 SQL 过来,查询目标是“找出所有薪资大于 5000 且入职超过一年的员工,或者所有年龄大于 30 的员工”。他写出来的条件是:

WHERE salary > 5000 AND hire_date < '2023-01-01' OR age > 30

结果查出来的数据里混进了一大堆薪资只有两三千、入职也没多久的年轻员工。他看着结果喊冤:这条件我写得没毛病啊,数据库是不是出 BUG 了?

没毛病?毛病大了。

缺失的括号把这句 SQL 的真实语义从“薪资高且老员工,或者年龄大的人”悄悄改成了“薪资高且老员工,或者所有年龄大的人”。后者比前者多出了一整批年龄大但薪资不高的员工。这就是 SQL 语句优先级在背后搞事情。

这类问题,几乎是每个写 SQL 的人都会遇到的坎。你以为自己“写对了”,但数据库是按它自己的规则去解析的。这篇文章就是把 SQL 里所有和优先级、执行顺序相关的坑一次性讲透,适合被 SQL 结果搞懵的开发者、数据分析师、数仓工程师,以及准备面试的同学。

1. 先把概念捋清楚:SQL 优先级到底指什么东西

1.1 优先级和执行顺序是两码事,别混在一起

我刚接触 SQL 那会儿,一直以为“优先级”就是“先算哪段条件”的问题。后来被几个报错折腾了几轮,才明白 SQL 里的优先级其实是两个维度的东西:

第一个维度是运算符优先级。比如 AND 和 OR 谁先生效,比较运算符和逻辑运算符谁先算,这个决定了条件表达式如何组合,属于“怎么组合”的问题。

第二个维度是语句内部各阶段的执行顺序。一条 SELECT 语句从字面上看是从 SELECT 写到 LIMIT,但数据库真正执行时是从 FROM 开始一路往后走的。这个顺序决定了你在 WHERE 里能不能用 SELECT 里定义的别名?能不能直接写聚合函数?窗口函数为什么必须套子查询?这些全部由执行顺序决定。

很多人“写对了报错”,本质上是混淆了这两个维度。你以为是语法问题,实际是解析和执行机制的问题;你以为条件组合对了,实际是运算符优先级把条件悄悄改了。

打个比方:运算符优先级就像数学里的“先乘除后加减”,执行顺序则像工厂流水线的工位顺序——原料先进第一道工序,再进第二道工序,你不可能在第三道工序用第一道工序还没生产出来的零件。SQL 的每个关键字,本质上就是在流水线上占了一个工位。

1.2 AND、OR、NOT 的优先级规则:90% 的条件组合问题都栽在这

先记住一条铁律:在 SQL 的逻辑运算符里,NOT 的优先级最高,AND 其次,OR 最低。

这意味着什么?看这句:

WHERE age > 30 AND gender = '男' OR salary > 5000

数据库不会按你从左到右的阅读习惯去理解,它会先处理 AND,把条件变成:

WHERE (age > 30 AND gender = '男') OR salary > 5000

于是结果是“三十岁以上的男员工”加上“所有月薪超过 5000 的人”。如果你本意是“三十岁以上的人,要么是男性,要么薪资高”,那就必须自己加括号:

WHERE age > 30 AND (gender = '男' OR salary > 5000)

这两个写法的结果集差异非常大。第一种写法会把所有高薪的人都选出来,哪怕人家又年轻又不是男性;第二种写法只会选三十岁以上、且满足“男性或高薪”的人。

我见过不少线上事故就是这么产生的。比如一个订单查询,条件是“已支付或 VIP 用户且订单金额大于 100”,结果少写一个括号,把大量小额非 VIP 订单查出来推送到了下游,数据直接跑偏了好几天才被发现。

AND 优先级高于 OR 这件事,其实符合大多数编程语言的习惯。但 SQL 和普通代码不一样,普通代码里每个条件的逻辑组合你会写成 if 嵌套或者用括号包清楚,SQL 条件一旦变多,很容易漏括号。所以我现在的习惯很固定:只要条件里同时出现 AND 和 OR,无条件全部加括号,哪怕语义本来就是从左到右的,也绝不依赖默认优先级。

再补一个 NOT 的坑。NOT 的优先级比 AND、OR 都高,所以NOT age > 30 AND salary > 5000等价于(NOT age > 30) AND salary > 5000,这个还好理解。真正容易翻车的是 NOT 和 NULL 组合的情况。SQL 里的比较结果不是简单的真和假,还有一个 UNKNOWN 状态。比如WHERE NOT (age > 30),它不会把age IS NULL的行选出来,因为 NULL > 30 的结果是 UNKNOWN,NOT UNKNOWN 还是 UNKNOWN,WHERE 只保留结果为 TRUE 的行。这个三值逻辑,后面讲 NOT IN 的时候还会再引发一次大地震,先记在心里。

2. 执行顺序才是“报错”的万恶之源

2.1 一条 SELECT 的真实执行链条:FROM 比 SELECT 先跑

很多 SQL 报错,从字面上看完全想不通。比如你在 WHERE 里用了一个 SELECT 子句里定义的别名,数据库甩给你一句Unknown column 'xxx' in 'where clause'。你检查了一遍又一遍,明明列名没拼错,怎么就不认识呢?

因为 WHERE 执行的时候,SELECT 还没执行呢。

标准的逻辑执行顺序是这样的:

FROM / JOIN WHERE GROUP BY HAVING SELECT DISTINCT ORDER BY LIMIT / OFFSET

注意,这不是数据库实际物理执行的顺序,优化器会做各种改写和重排。但它是 SQL 的“逻辑执行顺序”,决定了一条语句里每个部分能引用什么。所有语义层面的规则,都建立在逻辑顺序上。

SELECT 阶段都排第五位了,你在第三步的 WHERE 里想用第五步才生出来的别名,当然不行。看这个报错:

SELECT employee_id, salary * 1.1 AS new_salary FROM employees WHERE new_salary > 5000;

数据库在执行 WHERE 时,只有 employees 表里的原始列,new_salary这个别名还没生成。解决办法有两个:要么直接写原始表达式:

WHERE salary * 1.1 > 5000

要么套一层子查询,等别名生成后再过滤:

SELECT * FROM ( SELECT employee_id, salary * 1.1 AS new_salary FROM employees ) t WHERE new_salary > 5000;

ORDER BY 就没这个问题,因为 ORDER BY 排在 SELECT 后面,可以放心用别名。GROUP BY 的情况有点特殊,MySQL 默认允许在 GROUP BY 里用 SELECT 别名,但这是它的扩展行为,标准 SQL 以及其他很多数据库并不支持。为了代码跨库可读,我在 GROUP BY 里从来都是用原始表达式,不用别名。

2.2 聚合函数不能出现在 WHERE 里的原因:步骤顺序锁死了

还有一个高频报错:Invalid use of group function。这类错误通常长这样:

SELECT dept_id, AVG(salary) FROM employees WHERE AVG(salary) > 10000 GROUP BY dept_id;

很多人不理解:聚合函数 AVG 不是在 SELECT 里能用吗?为什么放到 WHERE 里就报错?

看执行顺序就明白了。WHERE 在第三步,GROUP BY 在第四步。聚合函数必须在分组之后才能计算,也就是说 AVG(salary) 这个值在 WHERE 阶段根本不存在。正确的做法是把这个条件放到 HAVING 里,因为 HAVING 是在 GROUP BY 之后执行的:

SELECT dept_id, AVG(salary) FROM employees GROUP BY dept_id HAVING AVG(salary) > 10000;

WHERE 和 HAVING 的分工,很多人背过“WHERE 过滤行,HAVING 过滤组”,但没想过为什么。背后的原理就是执行顺序:WHERE 必须先做行级过滤,不能让还没分组的聚合函数参与;等到 HAVING 执行时,分组已经完成,聚合结果已经算好,自然就能拿来过滤组了。

这段顺序不仅解释了报错,还解释了数据量的变化。比如你要统计每个部门的平均薪资,如果先 WHERE 过滤掉离职员工和实习生,聚合的数据集就小了很多。这个顺序决定了你的统计口径,同一条 SQL 如果把过滤条件写在 JOIN 的 ON 里而不是 WHERE 里,结果可能完全不一样,这就是下一节要说的 JOIN 优先级问题。

3. JOIN 场景下的优先级陷阱:ON 和 WHERE 不是一回事

3.1 LEFT JOIN 里条件写错位置,行数直接变少

JOIN 相关的“感觉写对了但结果不对”,最经典的必然是 LEFT JOIN 里的 ON 和 WHERE 之争。

LEFT JOIN 的逻辑是:先把左表所有行保留下来,然后拿着 ON 条件去右表找匹配行,匹配上了就把右表对应的列带过来,匹配不上右表列补 NULL。这是第一步。

等到 LEFT JOIN 做完,才轮到 WHERE 对结果集做过滤。这俩完全在两个阶段。

看一个具体的例子。订单表 orders 和支付流水表 payments,要查所有订单以及它们的支付状态:

SELECT o.order_id, o.amount, p.status FROM orders o LEFT JOIN payments p ON o.order_id = p.order_id AND p.status = 'SUCCESS';

这个写法,ON 里的p.status = 'SUCCESS'只是参与匹配规则:右表只有状态为 SUCCESS 的支付流水才有资格被连接上。那些没有成功支付的订单,左表的行还在,右边显示 NULL。结果集行数等于左表全部订单数。

但如果把同样的条件挪到 WHERE:

SELECT o.order_id, o.amount, p.status FROM orders o LEFT JOIN payments p ON o.order_id = p.order_id WHERE p.status = 'SUCCESS';

结果就完全变味了。WHERE 是在连接已经完成后执行的行级过滤,p.status = 'SUCCESS'会把所有没有成功支付记录的订单行直接过滤掉。行数从“全部订单”锐减成“只有成功支付的订单”,你还以为自己查的是全部订单,实际上悄悄丢了一大批数据。

这种问题在报表里特别致命。业务方说“我要看所有订单,包括没支付成功的”,结果你写出来的 SQL 因为一个条件位置不对,把没支付成功的全筛掉了。表面看数据没报错,实际口径偏得离谱。

想要“无论有没有成功支付都保留左表订单”,正确做法是用两个条件隔离 OR 的方式写 WHERE:

WHERE p.status = 'SUCCESS' OR p.status IS NULL

但更语义化的做法还是把状态条件留在 ON 里,让 WHERE 干净一点。RIGHT JOIN、FULL JOIN 的原理一样,只是保留哪一侧的表不同。FULL JOIN 两边都保留,ON 条件影响的是两边的匹配行为,WHERE 则会过滤连接后的行,逻辑同理。

3.2 INNER JOIN 和 CROSS JOIN:优先级影响的另一种打开方式

INNER JOIN 就省心得多。因为内连接本身会丢弃不匹配的行,ON 里写条件还是 WHERE 里写条件,结果集是一样的。比如INNER JOIN payments p ON o.id = p.id AND p.status='SUCCESS'和INNER JOIN payments p ON o.id = p.id WHERE p.status='SUCCESS',最终行数一致。区别只在代码可读性上:业内约定是 ON 放连接条件,WHERE 放过滤条件,方便后人维护。

CROSS JOIN 如果带了 WHERE 条件,本质上就等价于 INNER JOIN。比如FROM t1 CROSS JOIN t2 WHERE t1.id = t2.id,效果和 INNER JOIN 相同。没有 WHERE 的 CROSS JOIN 就是纯笛卡尔积,行数是两表行数相乘,这个数据量往往爆炸式增长,线上查询一定要警惕。

还有一个容易被忽略的点:多表 JOIN 的优先级。数据库优化器对 INNER JOIN 的连接顺序一般会自己重排,以达到最优执行效率,所以 INNER JOIN 多表情况下,表顺序对结果没有影响,性能却可能有天壤之别。但 LEFT JOIN 不一样,它是带方向的,交换表顺序会改变语义。比如:

FROM t1 LEFT JOIN t2 ON ... LEFT JOIN t3 ON ...

和

FROM t1 LEFT JOIN t3 ON ... LEFT JOIN t2 ON ...

如果 ON 条件之间互相依赖,两条 SQL 的结果集可能完全不同。复杂场景下,我会用括号显式控制 JOIN 的分组:

FROM (t1 LEFT JOIN t2 ON ...) JOIN t3 ON ...

括号在 FROM 子句里也是可以用的,它把“先连接谁”的语义固化下来,避免优化器或者维护人员理解偏差。

4. 子查询、运算符与窗口函数的优先级陷阱

4.1 子查询何时执行?派生表必须起别名

子查询的执行时机和它出现的位置有关。出现在 WHERE 里的普通子查询,会被执行后把结果返回给外层使用。出现在 FROM 里的子查询,则会被当成一张临时表来参与后续连接,这种子查询叫派生表。

派生表有个非常容易踩的报错,在 MySQL 里尤其常见:

SELECT * FROM ( SELECT employee_id, salary FROM employees WHERE salary > 5000 );

这语句看着没毛病吧?但 MySQL 会直接报错:Every derived table must have its own alias。意思是,FROM 子句里的子查询必须给它起个别名。正确写法是:

SELECT * FROM ( SELECT employee_id, salary FROM employees WHERE salary > 5000 ) AS t;

这个报错坑了无数新手。原因在于 FROM 阶段的子查询在逻辑上会被当作一张“派生表”供外层引用,没有名字就无法在后续语法解析中被引用。虽然你外层没引用它的字段,但解析规则就是一视同仁,必须给名字。

还有一个子查询相关的性能陷阱:关联子查询。所谓关联子查询,是指子查询内部引用了外层表的列。比如查每个部门薪资最高的员工:

SELECT employee_id, name, dept_id, salary FROM employees e WHERE salary = ( SELECT MAX(salary) FROM employees WHERE dept_id = e.dept_id );

这种写法逻辑没错,但执行上非常难受:对于外层表的每一行,数据库都要执行一次子查询。外层有多少行,子查询就跑多少遍。数据量一上来,性能就是灾难。这种场景我更推荐用窗口函数或者 JOIN + GROUP BY 改写,逻辑更清晰,性能也稳得多。

4.2 IN、EXISTS、ANY/ALL 的优先级与短路逻辑

IN、EXISTS、ANY、ALL 这组运算符,表面是“子查询的辅助工具”,实际上它们和优先级、短路、NULL 三值逻辑缠绕得很深。

先看最简单的情况:IN 等价于多个 OR 条件。所以WHERE dept_id IN (10, 20)等价于WHERE dept_id = 10 OR dept_id = 20。这也意味着,IN 和 OR 组合使用时,要小心 AND 的优先级参与进来。比如:

WHERE status = 'A' AND dept_id IN (10, 20) OR group_id = 5

数据库会把它解析成(status='A' AND dept_id IN (10,20)) OR group_id=5,如果你本意是“status 为 A 且部门在 10/20 或分组为 5”,同样需要括号。

EXISTS 和 IN 在逻辑上经常可以互换,但执行策略不一样。EXISTS 是只要子查询找到第一条匹配记录就会返回 TRUE,带有短路性质;IN 则往往要把子查询结果全部算出来再逐个比较。数据量大的场景下,EXISTS 通常更高效,但不是绝对,具体得看执行计划。

ANY 和 ALL 的坑主要又是在 NULL 身上。salary > ALL (SELECT salary FROM ...),如果子查询结果集为空,整个条件返回 TRUE,也就是说“比一张空表里所有人的薪资都高”竟然成立。如果子查询里有 NULL,比较结果会变成 UNKNOWN,行被过滤。很多人在写“比所有人薪资都高”的查询时,发现结果集莫名其妙少了员工,排查半天才发现是某一行薪资为 NULL 把整个比较结果带崩了。处理办法是先过滤 NULL 或用 COALESCE 给默认值。

UNION 也有自己的优先级特点。在标准 SQL 里,UNION 的优先级高于 ORDER BY 和 LIMIT。也就是说:

SELECT id FROM t1 UNION SELECT id FROM t2 ORDER BY id;

ORDER BY 作用在整个 UNION 的结果集上,而不是最后一条 SELECT 上。如果你想让每段 SELECT 各自排序,必须给子查询加括号:

(SELECT id FROM t1 ORDER BY id LIMIT 5) UNION SELECT id FROM t2;

不加括号的时候,前面的 ORDER BY 甚至可能被数据库直接忽略,尤其 MySQL 里,这个行为让我吃过不少亏。UNION 的列名以第一个 SELECT 为准,也是另一个容易让人困惑的规则,本质上也是语法解析优先级的一种体现。

4.3 窗口函数为什么被 WHERE 拒之门外

MySQL 8.0 普及之后,窗口函数用得越来越多。随之而来的一个经典报错是:

Window function is not allowed in WHERE clause

比如:

SELECT employee_id, salary, RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk FROM employees WHERE rk = 1;

报错原因非常直接:窗口函数是在 WHERE 执行之后才开始计算的。按照执行顺序,FROM/JOIN 先做,WHERE 再对行做过滤,GROUP BY/HAVING 做完后,窗口函数才在 SELECT 阶段或之前的一个专门步骤里计算。你在 WHERE 里引用 rk,此时 rk 根本不存在。

正确做法是套一层子查询,先算好窗口函数,再在外层过滤:

SELECT * FROM ( SELECT employee_id, salary, RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk FROM employees ) t WHERE rk = 1;

同样的逻辑也适用于在 HAVING 里直接使用窗口函数,很多数据库同样不允许。遇到这类需求,统一思路:子查询包一层,把窗口函数的结果变成普通列,再交给外层去过滤、分组、排序。

5. 报错排查速查表与我的排查路径

5.1 高频报错成因对照表

积累了足够多的踩坑经验后,我整理了一张速查表。以后你只要看到类似的报错或异常结果,直接对照着排查,能省下大量无头苍蝇式的时间。

现象真正原因解决方向
Unknown column 'xxx' in 'where clause'WHERE 阶段还不能引用 SELECT 阶段的别名用原始表达式,或子查询包装后再过滤
Invalid use of group functionWHERE 阶段聚合函数尚未生成聚合条件移到 HAVING
Every derived table must have its own aliasFROM 子句中派生表未命名给子查询加别名
Window function is not allowed in WHERE窗口函数在 WHERE 之后才计算子查询包一层,外层过滤
查询结果莫名多出一批数据AND、OR 优先级导致条件组合偏移给多种逻辑运算符组合加括号
LEFT JOIN 后左表行数变少WHERE 过滤连接后的结果集左表保留需求用 ON 携带条件
NOT IN 子查询结果为空子查询结果含 NULL,三值逻辑导致全表被过滤用 NOT EXISTS 替代
GROUP BY 报错列不在分组里严格 SQL 模式下 SELECT 列必须匹配 GROUP BY 列补全分组列,或对非聚合列用 MAX/MIN/ANY_VALUE 包裹

这张表里的最后一行值得一提。MySQL 5.7 之后默认开启 ONLY_FULL_GROUP_BY 模式,很多人写“SELECT dept_id, name FROM employees GROUP BY dept_id”,在旧版本能跑,新版本直接报错。因为 name 列没有出现在 GROUP BY 里,也没有被聚合函数包裹。这不是优先级问题,但它是典型的“同一个 SQL 换个环境就报错”的场景,严格模式把不规范写法直接拦截了。

5.2 排查 SQL 问题时我固定走的四步路径

第一步,格式化。把 SQL 写成分层缩进的风格,每个关键字单独一行,每个 AND/OR 条件单独一行,一眼就能看清条件都在哪个层级。很多诡异的报错,格式化之后自己就看出来了。

第二步,找条件位置。看每一条过滤条件写在哪个子句里,ON、WHERE、HAVING 各自对应不同阶段。问自己一句:这个条件真的在这里吗?LEFT JOIN 的条件在 WHERE 里是否会影响左表行数?窗口函数和聚合函数出现在 WHERE 里就是违规。

第三步,加括号显式化。把所有的 AND/OR 组合都用括号写清楚。哪怕数据库觉得冗余,也要写。括号不是给数据库看的,是给“下一个维护这段代码的人”看的。这个人大概率就是几个月后的你自己。

第四步,拆分验证。把复杂 SQL 拆成一段一段独立执行,先查子查询单独的结果,再套到外层逐步验证。哪个环节数据开始不对,问题就暴露在哪里。别一口气调完整条大 SQL,那样很难定位范围。

另外,EXPLAIN 一定要学会看。它展示优化器实际的执行计划,比如连接的顺序、是否用了索引、扫描了多少行。它会让你看到数据库真实干了什么,而不是你脑子里以为它应该干什么。不过要留意一点,EXPLAIN 显示的执行细节很多时候和逻辑执行顺序不完全一致,优化器会改写语句。你可以在心里把“逻辑执行顺序”当作语义规则,把“执行计划”当作物理实现,两者分开理解,不冲突。

最后分享一点个人体会

被 SQL 优先级坑过几轮之后,我写 SQL 的习惯变得非常保守。只要有逻辑组合,就无条件加括号;但凡看到“这 SQL 明明写对了怎么还报错”的情况,先去想执行顺序,而不是怀疑数据库有问题。另一个很有用的习惯是,每次改完一条复杂的查询,我都会顺手记录下查询预期的行数,比如注释里写一句“期望行数 = 全量订单数”,跑完 SQL 后对比一下。这个习惯帮我在 LEFT JOIN 丢行、AND/OR 组合偏移这类问题上提前拦下了好几次数据事故。

SQL 的优先级规则说穿了不难,难的是在真实业务场景里始终保持警惕。如果你看完这篇,能记住一句话,那就记这句:条件组合不确定就加括号,执行阶段不确定就看顺序,结果诡异不确定就拆开验证。够用了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询