在 SQL 关联查询中,需求经常不是“只要两边都能匹配上的记录”,而是“结果里必须出现某张表的全部数据”。例如订单表关联用户表,希望列出所有用户,即使用户没有下过单,也要在结果中保留一行。LEFT JOIN 正是为了实现这种“左表全量保留”的语义。

一、LEFT JOIN 的核心语义与基础语法
LEFT JOIN 也叫左外连接,它的执行逻辑可以概括为:先把左表的所有行都放进结果集,然后拿右表去匹配连接条件;能匹配上的行,拼接右表对应字段;匹配不上的行,右表部分以 NULL 填充。因此,左表的行数在连接之后不会因为右表缺失而减少,这是与 INNER JOIN 最本质的区别。
假设有两张表:users 用户表保存用户基础信息,orders 订单表保存下单记录。用户表有 4 个用户,订单表只有两个用户有订单。如果直接使用 INNER JOIN,结果只会出现有订单的用户;而使用 LEFT JOIN,结果会保留全部 4 个用户,没有订单的用户对应的订单字段为 NULL。
-- 用户表 CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(50) ); -- 订单表 CREATE TABLE orders ( id INT PRIMARY KEY, user_id INT, amount DECIMAL(10,2) ); INSERT INTO users VALUES (1, '张三'), (2, '李四'), (3, '王五'), (4, '赵六'); INSERT INTO orders VALUES (101, 1, 99.00), (102, 2, 199.00), (103, 2, 59.00); SELECT u.id, u.name, o.id AS order_id, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id;
上面的查询会返回 5 行:张三 1 行,李四 2 行,王五和赵六各 1 行。王五和赵六没有订单,order_id 和 amount 都是 NULL。注意李四有两条订单,所以左表的一行会对应右表两行,左表数据被复制,这是关联查询的正常行为,不是数据错误。
理解这个语义后,可以推导出一个判断标准:如果需要统计某张表的所有实体,不管另一张表有没有关联记录,就用左外连接;如果只需要公共交集,就用内连接。
二、ON 条件和 WHERE 条件的区别
这是 LEFT JOIN 使用中最容易踩坑的地方。很多人会写出类似下面的 SQL,希望过滤掉金额小于 100 的订单,同时以为没有订单的用户还会保留:
-- 错误写法:把过滤条件放在 WHERE 中 SELECT u.id, u.name, o.id AS order_id, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.amount > 100;
这条 SQL 的实际效果是:先做左连接,生成包含 NULL 的中间结果;然后 WHERE o.amount > 100 对中间结果进行过滤。因为 NULL 与 100 比较的结果不是 TRUE,那些没有订单的用户行会被直接丢弃,王五、赵六消失。最终结果和 INNER JOIN 加同样条件几乎一样,这就失去了左连接保留左表全部数据的意义。
正确的做法是把针对右表的过滤条件写进 ON 子句:
-- 正确写法:过滤右表条件放在 ON 中 SELECT u.id, u.name, o.id AS order_id, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.amount > 100;
这样语义变成:连接时只匹配订单金额大于 100 的右表记录;金额不大于 100 的订单不参与匹配,左表用户仍然保留。张三有一条 99 元订单,因为不满足 ON 条件,所以张三也会显示为 NULL,整条用户记录不会消失。
可以简单记为:ON 决定哪些右表行可以和左表行拼接,不影响左表行是否保留;WHERE 决定最终结果集保留哪些行,会过滤掉不想保留的左表行。 如果要对左表本身做筛选,比如只查某个城市的用户,可以放在 WHERE 中,但要注意此时的过滤是针对左表字段,不会破坏左表全量语义。
三、典型业务场景:统计零订单用户与补全维度
LEFT JOIN 在日常开发中最常见的用途之一,是找出“有主记录但没有关联明细”的数据。比如运营人员想看哪些用户从来没下过单,SQL 可以这样写:
SELECT u.id, u.name FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.id IS NULL;
这里先左连接保留所有用户,再用 WHERE o.id IS NULL 找出右表完全没匹配上的记录。这个写法和前面说的 WHERE 过滤有区别:它过滤的是右表的主键是否为 NULL,而不是右表普通字段的大小比较。NULL 判断条件是明确的,不会错误删除左表数据,反而正是利用左连接产生的 NULL 来定位缺口。
另一个典型场景是报表补全维度。假设要统计每个用户的累计消费金额,没有订单的用户也要显示为 0,可以使用 LEFT JOIN 配合聚合函数:
SELECT u.id, u.name, COALESCE(SUM(o.amount), 0) AS total_amount FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id, u.name;
这里 SUM(o.amount) 对没有订单的用户会返回 NULL,COALESCE 把 NULL 转成 0。但要注意,如果直接写 COUNT(o.id) 统计订单数,没有订单的用户返回 0 而不是 NULL,因为 COUNT 会忽略 NULL 值,这恰好符合预期;如果写 COUNT(*) 则会统计整行,没有订单的用户会得到 1,属于统计错误。
多表连续左连接也常见。例如用户表左连接订单表,再左连接收货地址表,可以同时保留没有订单的用户和没有地址的订单。关键是每一层左连接都以当前结果集的左表为基准,连接顺序会影响最终行数。如果中间某层用了 INNER JOIN,前面保留的未匹配行又会被过滤掉。
四、常见问题与优化建议
第一个常见问题是数据重复。左连接时,如果右表有多条匹配记录,左表行会被复制。例如用户表左连接订单表,李四有两条订单,结果中出现两行李四。如果业务上只需要每个用户一行,可以在连接前先对右表做去重或聚合,而不是连接后再 DISTINCT,后者通常会带来更大的数据扫描量。
第二个常见问题是 NULL 带来的判断失误。左连接未匹配的字段是 NULL,如果右表字段本身也可能为 NULL,就不好区分是“原本没有值”还是“没有匹配上”。这时可以优先使用右表主键或非空字段做 IS NULL 判断,例如 o.id IS NULL,避免使用可能为 NULL 的普通业务字段。
性能方面,LEFT JOIN 的关键在于连接字段的索引。数据库通常会以左表为驱动表,逐行去右表匹配。如果右表连接字段没有索引,每行左表记录都可能触发全表扫描,数据量大时性能会急剧下降。因此应确保 ON 子句中的关联字段在右表上有索引。对于 MySQL,可以用 EXPLAIN 查看执行计划,确认是否使用了 ref 或 eq_ref 访问类型。
另外,如果业务上只需要左表中那些右表无法匹配的记录,也就是反连接场景,可以考虑使用 NOT EXISTS 或 NOT IN。不过 NOT IN 遇到子查询返回 NULL 时会有语义陷阱,NOT EXISTS 通常更安全。LEFT JOIN + IS NULL 在某些数据库优化器下也能转换成反连接执行,但写法上要清晰,避免让后续维护的人误解。