首页/数据/SQL 速查/LEFT JOIN 左外连接

LEFT JOIN 左外连接

保留左表全部行,右表没有匹配时用 NULL 补齐;适合“主表 + 可选关联信息”的查询。

SQL 速查数据更新于 2026-10-13

LEFT JOIN 是什么

LEFT JOIN 左外连接以左表为基准,保留左表的全部行,右表没有匹配时用 NULL 补齐右表那几列。它解决的是主表加可选关联信息这类查询:用户列表带最近一次登录、订单带可选绑定的优惠券。写外连接时,属于右表的过滤条件必须放在 ON 里;放在 WHERE 里会把补 NULL 出来的行过滤掉,查询结果静默退化成内连接,少了数据也不报错。

基本信息

作用保留左表全部行,右表缺匹配补 NULL
语法SELECT … FROM a LEFT JOIN b ON a.id = b.a_id
等价写法LEFT OUTER JOIN,OUTER 可省略
右表过滤条件写 ON;写 WHERE 会过滤掉补 NULL 的行
行数下限不小于左表行数;左表一行可被放大成多行
适用平台标准 SQL,MySQL 8.0 与 PostgreSQL 均支持

典型使用场景

  • 主表带可选关联:用户列表显示最近一次登录时间,没有登录记录也保留用户行
  • 找缺失数据:找出从未下单的用户,配合 WHERE 右表主键 IS NULL
  • 报表补零:日期维度表左连接事实表,没有数据的日期保留为 0 而不是整行消失
  • 软关联:订单可选绑定优惠券、可选绑定推荐人
  • 多表串联:A 左连 B 再左连 C,每一步都可能补出新的 NULL,需要逐段验证
  • 数据比对:新旧系统用户表左连接,找出只在旧系统存在的账号

基础示例

基础示例
SELECT u.id,
       u.name,
       o.order_no,                              # 没有订单时为 NULL
       o.amount
FROM users AS u                                 # 左表:全部用户
LEFT JOIN orders AS o ON o.user_id = u.id       # 连接条件写在 ON
ORDER BY u.id;

找未匹配与错误写法

找未匹配与错误写法
-- 正确:左连接再加右表主键 IS NULL
SELECT u.id FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;

-- 等价:NOT EXISTS,语义不受 NULL 影响
SELECT u.id FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

-- 错误示范:右表条件写进 WHERE,没有订单的用户被过滤掉
SELECT u.id, o.order_no
FROM users u LEFT JOIN orders o ON o.user_id = u.id
WHERE o.status = 'paid';

条件位置的差异

条件放哪对左表未匹配行的影响结果
ON 里加右表条件左表行仍保留,右表列补 NULL仍是外连接,只是收窄匹配范围
WHERE 里加右表条件补 NULL 的行条件为 UNKNOWN 被丢弃退化为内连接
WHERE 里加左表条件只过滤左表行,不改变连接语义可以安全使用
WHERE 右表列 IS NULL专门筛出未匹配行反连接的常见写法
把右表过滤条件写在 WHERE 是外连接最高频的错误,结果集静默变成内连接,数据少了却不报错,排查时要对照两边行数。
判断是否匹配要用右表主键或非空列做 IS NULL(例如 o.id IS NULL),用可能为空的业务列判断会把真实存在的行也当成未匹配。
左连接会把左表行放大:左表一行匹配右表多行时结果行数增加,直接 SUM 会重复计数;先在子查询里把右表聚合到一行再连接。
多级左连接时中间表补出的 NULL 会继续向下传递,条件越多越难定位;建议每连一张表就单独跑一次看行数,确认没有意外放大。

先把右表聚合到一行

先把右表聚合到一行
-- 左连接明细表会放大行数,先按用户聚合再连接
SELECT u.id, u.name, t.order_cnt, t.total_amount
FROM users AS u
LEFT JOIN (
    SELECT user_id,             # 按用户聚合
           COUNT(*) AS order_cnt,
           SUM(amount) AS total_amount
    FROM orders
    WHERE status = 'paid'       # 过滤放在子查询里,语义更清楚
    GROUP BY user_id
) AS t ON t.user_id = u.id;     # 连接粒度是用户,不会放大

写法要点

  • 左连接后接右连接的过滤,条件一律放 ON,除非确实想过滤掉未匹配行
  • 用 LEFT JOIN 再对右表列做 IS NULL,是反连接的经典写法,但可读性不如 NOT EXISTS
  • 需要区分未匹配与匹配但值为空时,加一个布尔列:o.id IS NOT NULL AS has_order
  • MySQL 不支持 FULL JOIN,需要 FULL 语义时用 LEFT JOIN 与 RIGHT JOIN 的结果做 UNION
  • ON 与 WHERE 里的条件顺序不影响结果,但把连接条件与过滤条件分开写可读性明显更好
  • 外连接的结果行数先算出来再聚合,能避免把重复计数问题带到报表口径里

易错点

  • 右表过滤条件写在 WHERE,外连接静默退化为内连接
  • 用右表可能为 NULL 的业务列做 IS NULL 判断,会把真实匹配的行误判为未匹配
  • 左表一行匹配右表多行时结果行数被放大,直接 SUM 或 COUNT(*) 会重复计数
  • 多级左连接时 NULL 逐层向下传播,不逐段验证行数很难定位放大点
  • 外连接后对右表列做 NOT IN 子查询,NULL 参与比较会让结果整体为空,改用 NOT EXISTS

数据来源与参考

  • MySQL 8.0 Reference Manual · LEFT JOIN
  • PostgreSQL Documentation · LEFT OUTER JOIN
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌