一个 LEFT JOIN 的空行全没了|搞懂 SQL 执行顺序才明白

简介: LEFT JOIN 过滤条件写进 WHERE 导致左表行丢失,讲清执行顺序,复现 LEFT JOIN 变 INNER JOIN 与别名失效,区分逻辑顺序与执行计划,给出 ON、WHERE 判定规则。

大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。

有段时间我写 SQL,总以为数据库是按我写的顺序从上往下跑的。写什么,它就按什么顺序执行。结果一个 LEFT JOIN 的过滤条件,被我写进了 WHERE。跑完一看,左边表里没配对的行全没了。看着像 LEFT JOIN,结果出来的跟 INNER JOIN 一模一样。更气人的是,它不报错。那次之后我才搞清楚,SQL 的书写顺序和执行顺序,是两码事。搞混了,语法上挑不出毛病,结果就是不对。这种错最难查,只能拿数据一行行对。

真实的执行顺序

数据库跑一条 SQL,不是从上往下读。它按一个固定的逻辑顺序走,分阶段处理。

FROM → ON → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT

每一步的输出,是下一步的输入。这个顺序决定了你能在哪个阶段用什么东西,也决定了条件放哪个位置。最关键的几个分水岭。ON 和 JOIN 在 WHERE 之前,所以写在 ON 里的条件影响的是连接时的保留规则,而写在 WHERE 里的条件,是在连接完成后对结果再做过滤。SELECT 在 WHERE 之后,所以别名在 WHERE 里还不存在。HAVING 在 GROUP BY 之后,过滤的是聚合后的结果。顺序不用死记。后面两个翻车现场,一看就知道错在哪一步。

翻车一:LEFT JOIN 变成 INNER JOIN

来看我踩的那个坑。一张用户表,一张订单表。我想查所有用户以及他们状态为 1 的订单,没订单的也要留着。

SELECT u.user_name, o.order_no, o.amount
FROM t_user u
LEFT JOIN t_order o ON u.user_id = o.user_id
WHERE o.status = 1;

看着没问题。但跑出来的结果里,那些还没下过单的用户,一条都没了。为什么。LEFT JOIN 的语义是:左边表的所有行都保留,右边表匹配不到的行补 NULL。这一步做完,新用户的订单字段全是 NULL。然后轮到 WHERE,o.status = 1 这个条件一过滤,NULL = 1 为假,新用户的行就被扔掉了。连接阶段保住的行,在 WHERE 阶段又丢了。结果就是 LEFT JOIN 被 WHERE 条件吃掉了外层,退化成 INNER JOIN。

正确的写法,是把右边表的过滤移到 ON 里。

SELECT u.user_name, o.order_no, o.amount
FROM t_user u
LEFT JOIN t_order o ON u.user_id = o.user_id AND o.status = 1;

ON 里的条件在连接时就生效,o.status = 1 作为连接条件的一部分,不会把左表未匹配的行筛掉。来对比一下两种写法的结果差异,数值是示意,实机跑一遍更准。

CREATE TABLE t_user (user_id INT PRIMARY KEY, user_name VARCHAR(20));
CREATE TABLE t_order (order_id INT PRIMARY KEY, user_id INT, status INT, order_no VARCHAR(20), amount DECIMAL(10,2));
INSERT INTO t_user VALUES (1,'Alice'),(2,'Bob'),(3,'Carol');
INSERT INTO t_order VALUES (101,1,1,'ORD001',50.00),(102,1,2,'ORD002',30.00);
-- Carol 没有订单
写法 返回行数 Carol 在结果里吗
WHERE o.status = 1 1 行(Alice 的 ORD001) 不在
ON ... AND o.status = 1 3 行(Alice 一条、Bob 一条补 NULL、Carol 一条补 NULL) 在

翻车二:WHERE 里用不了别名

另一个坑,出在别名上。我想查总金额大于 100 的明细。

SELECT user_id, amount * quantity AS total
FROM t_item
WHERE total > 100;
-- ERROR 1054: Unknown column 'total' in 'WHERE'

报错说列不存在。但 total 明明在 SELECT 里定义了。原因在顺序。WHERE 在 SELECT 之前执行。数据库跑到 WHERE 那一步时,total 这个别名还没诞生,它不知道你在指什么。解决办法有三个,一是重复表达式。

SELECT user_id, amount * quantity AS total
FROM t_item
WHERE amount * quantity > 100;

二是用子查询,把别名提出来。

SELECT * FROM (
  SELECT user_id, amount * quantity AS total
  FROM t_item
) t WHERE t.total > 100;

三,在 MySQL 里可以用 HAVING 代替 WHERE 来过滤别名,因为 HAVING 在 SELECT 之后。

SELECT user_id, amount * quantity AS total
FROM t_item
HAVING total > 100;

但要注意,HAVING 是过滤聚合结果的,拿来当 WHERE 用虽然 MySQL 能跑,但可读性差,而且可能影响优化器的走法。能用 WHERE 就用 WHERE,HAVING 留给聚合场景。

逻辑顺序与执行计划,不是一回事

上面说的是逻辑顺序,SQL 语义上的阶段顺序。但优化器在真正执行时,可以重排。比如一个 WHERE 过滤条件,优化器可能把它"下推"到表扫描阶段,先在扫描时就过滤掉大量不匹配的行,再做 JOIN。比如 EXPLAIN 里看到 rows 很少,说明下推生效了。比如多个表的 JOIN 顺序,优化器可能把小表当驱动表,而不是你 SQL 里写的顺序。

这些重排是性能层面的,不影响语义结果。优化器保证重排前后的结果一样,只是在更快。所以 EXPLAIN 看的不是逻辑顺序,是优化器优化后的物理执行路径。两者都对:逻辑顺序决定你的语句对不对,执行计划决定它快不快。写 SQL 时想的是逻辑顺序,调性能时才去看执行计划。

写 SQL 时,条件到底该放哪

摸清规律,就几条判定规则。OUTER JOIN(LEFT / RIGHT)时,右边表的过滤条件,优先放 ON。放在 WHERE 里,会把外层吃掉。除非你的本意就是只保留匹配的行——那直接写 INNER JOIN 就行,别用 LEFT JOIN 再在 WHERE 里过滤。INNER JOIN 时,ON 和 WHERE 放哪都行,结果一样。但从可读性出发,连接条件放 ON,结果过滤放 WHERE,分工清楚。

聚合过滤,WHERE 过滤原始行,HAVING 过滤聚合结果。别把 SUM()、COUNT() 塞进 WHERE。

别名只在 SELECT 之后可用。ORDER BY 和 HAVING 里可以用别名,WHERE 里不行。

判断力

SQL 最难的地方,不是语法多复杂,是写的人不知道数据库在后台按什么顺序跑。书写顺序是给人看的,执行顺序是给机器跑的。把两者搞混,就会写出"看着对、结果错"的语句。最可怕的是它不报错,你只能靠对照数据才能发现不对。

避坑清单

OUTER JOIN 的过滤条件放 ON,别放 WHERE。放 WHERE 里会把外层吃掉,LEFT JOIN 变 INNER JOIN,不报错,结果直接错。

别名只属于 SELECT 之后的阶段。WHERE 里引用别名会报错,HAVING 里能用但别当 WHERE 替代方案用。

最后一条是我自己踩的。我一直以为 SQL 是按书写顺序跑的,直到 LEFT JOIN 的空行全没,才去查了这个顺序。现在每写一条带 JOIN 的 SQL,我都会问自己一遍:这个条件,是在连接之前还是之后生效?问完再落笔,比跑完再看结果省事得多。

写在最后

写 SQL 久了会发现,语法只是表层。真正决定结果对不对的,是你脑子里那条流水线。数据从 FROM 进来,经过 ON、WHERE、GROUP BY,每一步之后数据长什么样,别名到哪一步才存在,你得心里有数。数据库只负责跑,不会提醒你顺序想错了。下笔前先在脑子里过一遍,很多错根本不会发生。

你写 SQL 时,条件放 ON 还是 WHERE 是怎么判断的?有没有因为放错位置翻过车?评论区聊聊。

我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋

相关文章
|
19小时前
|
JSON 自然语言处理 并行计算
多语言结构化决策开源软件:Deepopen(改进后的 Laya)的非自回归路由与概率校准设计
Deepopen(改进后的 Laya)是一款面向结构化决策场景的开源 System 1 决策引擎,可对文本、邮件、工单和 JSON 状态直接输出分类、评分与二元概率结果。系统通过非自回归推理、语言路由和概率校准,服务于多语言工单分诊、内容审核、提示词防护与模型路由等低延迟流程。其在 CLINC150 与 Banking77 的本地复现对照中分别位于第 2 位和第 5 位。
|
15小时前
|
存储 弹性计算 小程序
阿里云99元服务器一年在哪购买?续费多少钱?配置是什么?
阿里云99元/年ECS经济型e实例(2核2G、3M独享带宽、40G ESSD盘),新老用户同价,支持续费锁定99元/年(截至2029.03.31),适合学生测试、个人博客及轻量开发,不适用于高并发生产环境。阿里云服务器ECS官网:https://t.aliyun.com/U/AZBUsA
27 0
|
17小时前
|
弹性计算 人工智能 缓存
阿里云轻量应用服务器38元抢购注意事项:具体配置、抢购时间与抢购资格、适用场景等常见问题解析
阿里云38元/年轻量应用服务器(2核2G、40G ESSD、200Mbps峰值带宽、流量不限)适合Linux学习、个人博客、测试环境及轻量应用部署。参与需实名认证并满足新用户资格,仅在每日10:00、15:00两场限量秒杀,价格不可叠加优惠、不可变更配置;2GB内存难以同时承载数据库与多服务。若未抢到,可考虑68元新用户价或99元经济型e实例、199元u1实例等备选。
|
18小时前
|
人工智能 并行计算 固态存储
Wan2.2-Animate 万相 Animate 视频整合包(萝卜版 )本地化部署技术指南|AI 短剧角色动作迁移与视频换脸解决方案
万相Animate是基于阿里Wan2.2-Animate 14B的轻量化视频生成整合包,支持Move(图生动作)和Mix(角色替换)两大模式。内置Python/CUDA环境与FP8量化模型,最低仅需RTX3060/4060 8G显存,开箱即用,专为AI短剧、漫剧本地批量创作优化。(239字)
|
18小时前
|
API
💰千问办公企业标准版和旗舰版如何收费?看这篇文章就懂了~
阿里云千问办公企业版分标准版(198元/月/席)与旗舰版(298元/月/席,100席起购),含积分、多模型支持、管理后台及VPC部署等能力,支持积分包增购。详情见官网。阿里千问办公QwenWork官网:https://t.aliyun.com/U/0VCTGt
|
18小时前
|
JSON 前端开发 API
Flash 模型 Bechmark 横评
对轻量模型的能力做一些横评
64 1
|
18小时前
|
弹性计算
阿里云服务器2核4G、5M固定带宽、80G系统盘199元1年,续费同价(终于找到了)
阿里云199元/年服务器(2核4G/5M带宽/80G ESSD)未在特价页显示,但企业用户可通过ECS自定义购买渠道下单,新老用户均可,续费同价,需严格按指定配置选择。阿里云服务器ECS官网:https://t.aliyun.com/U/AZBUsA
|
19小时前
|
人工智能 Linux API
Token Plan是什么?介绍、收费、使用完整教程,看这一篇文章就够了!
阿里云百炼推出的Token Plan是AI大模型订阅服务,采用Credits统一计费,支持Claude、Cursor、Qwen Code等主流编程工具,省钱高效。含个人/团队多档套餐,提供API Key与快捷配置。在阿里云百炼官网:https://t.aliyun.com/U/fPVHqY 免费领取千万Tokens
25 0
|
19小时前
|
数据采集 人工智能 自然语言处理
基于阿里云百炼Agent Studio:智域蒲公英AI+ GEO工具多智能体架构演进与工程实践
在生成式引擎优化(GEO)场景中,如何解决长尾词按决策阶段分层的难题?如何保证大模型生成内容的“高可引用性”?本文以赛诺贝斯推出的“智域蒲公英AI+ GEO工具”为工程实践样本,深度拆解其基于阿里云百炼Agent Studio搭建的四大核心Agent系统。探讨Knowledge Studio知识库RAG切分、DashVector语义检索及通义千问在GEO复杂系统下的工程化落地经验
|
19小时前
|
SQL 存储 Java
微服务链路追踪不只是埋点:异步断链、动态回溯等关键问题拆解
微服务排障为何失效?本文从 Trace/Span 数据模型、探针与 OpenTelemetry 采集方式,到瀑布图下钻、依赖拓扑、资源联动分析,系统拆解异步线程池断链、头采样丢故障、容器环境动态回溯等落地难点,并给出可执行的接入路径与避坑建议。