原创

SQL 左连接、右连接与内连接的使用与区别

为什么连接查询一定要先分清“主表”和“保留行”

在 Java 后端开发里,业务查询很少只查一张表。用户列表要带部门名,订单列表要带用户信息,权限查询要关联角色和菜单,这些场景都离不开 JOIN

真正容易出错的地方,不是 JOIN 语法本身,而是没有搞清楚两件事:

  1. 谁是主表
  2. 哪张表的数据必须保留下来

理解这两点后,左连接、右连接、内连接的区别就非常清楚了。


先准备一组演示数据

下面用“用户表”和“订单表”举例。

DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS users;

CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    status TINYINT NOT NULL DEFAULT 1
);

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    order_no VARCHAR(50) NOT NULL,
    amount DECIMAL(10, 2) NOT NULL,
    CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id)
);

插入测试数据:

INSERT INTO users (id, username, status) VALUES
(1, 'alice', 1),
(2, 'bob', 1),
(3, 'charlie', 0),
(4, 'david', 1);

INSERT INTO orders (id, user_id, order_no, amount) VALUES
(101, 1, 'ORD-20260401-001', 99.00),
(102, 1, 'ORD-20260401-002', 199.00),
(103, 2, 'ORD-20260402-001', 59.00);

这组数据有几个关键点:

  • alice 有 2 个订单
  • bob 有 1 个订单
  • charlie 没有订单
  • david 也没有订单

这正好能看出不同连接方式的差异。


内连接:只要两边都能匹配上的数据

INNER JOIN 只返回两张表中能够关联成功的记录

SELECT
    u.id,
    u.username,
    o.order_no,
    o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

结果逻辑上等价于:

alice   ORD-20260401-001   99.00
alice   ORD-20260401-002   199.00
bob     ORD-20260402-001   59.00

可以看到:

  • charliedavid 被过滤掉了,因为他们没有订单
  • 订单表里如果出现没有对应用户的数据,也不会被查出来

内连接的核心特点

  • 只保留关联成功的数据
  • 两边任意一边缺失,结果就不会出现
  • 最适合“必须有关联数据才有业务意义”的场景

常见使用场景

  • 查询“有订单的用户”
  • 查询“已分配角色的账号”
  • 查询“存在有效明细的主单”

左连接:以左表为准,左边数据全部保留

LEFT JOIN保留左表的全部记录,右表匹配不上时,用 NULL 补齐。

SELECT
    u.id,
    u.username,
    o.order_no,
    o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

结果逻辑上类似:

alice    ORD-20260401-001   99.00
alice    ORD-20260401-002   199.00
bob      ORD-20260402-001   59.00
charlie  NULL               NULL
david    NULL               NULL

左连接的核心特点

  • 左表全保留
  • 右表匹配不上时,右侧字段为 NULL
  • 本质上是在问:“以左表为主,顺便看右表有没有对应数据”

常见使用场景

  • 查询所有用户及其订单信息
  • 查询所有部门及部门下员工信息
  • 查询所有菜单及是否已分配给某个角色
  • 查询主数据,并附带可选的关联信息

在业务代码里,左连接通常比内连接更常见,因为很多接口都要求“主表数据必须完整返回”。


右连接:以右表为准,右边数据全部保留

RIGHT JOIN 和左连接逻辑完全对称:保留右表全部数据,左表匹配不上则补 NULL

SELECT
    u.id,
    u.username,
    o.order_no,
    o.amount
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;

对于当前示例,由于订单都能找到用户,结果看起来和内连接一致:

alice   ORD-20260401-001   99.00
alice   ORD-20260401-002   199.00
bob     ORD-20260402-001   59.00

但它的语义和内连接不一样:

  • 内连接:只返回匹配成功的数据
  • 右连接:订单表全部保留
  • 如果某条订单没有匹配到用户,这条订单仍然会出现,只是用户字段是 NULL

右连接为什么在项目里用得少

因为它几乎总能改写成左连接。

例如:

SELECT
    u.id,
    u.username,
    o.order_no,
    o.amount
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;

可以改写为:

SELECT
    u.id,
    u.username,
    o.order_no,
    o.amount
FROM orders o
LEFT JOIN users u ON o.user_id = u.id;

两者语义一致,但后者通常更容易读。

很多团队会统一约定:只用 LEFT JOIN,尽量不用 RIGHT JOIN。这样 SQL 阅读成本更低,主表更直观。


三者最本质的区别

可以直接记成一句话:

  • 内连接:两边都匹配才返回
  • 左连接:左边全留,右边匹配不上补 NULL
  • 右连接:右边全留,左边匹配不上补 NULL

下面是一个更实用的理解方式:

1. 先看业务上谁是主数据

如果“用户”是主数据,那就应该让 users 放在左边,用 LEFT JOIN

2. 再看关联表是必需还是可选

  • 必须存在:INNER JOIN
  • 可以不存在,但主数据必须返回:LEFT JOIN

3. 右连接通常只是写法问题,不是能力问题

能用右连接解决的,大多也能通过调整表顺序改成左连接。


一张图理解三种连接

假设:

  • 左表:users
  • 右表:orders

那么可以这样理解:

INNER JOIN  = 只取 users 和 orders 的交集
LEFT JOIN   = 取全部 users,再补上能匹配到的 orders
RIGHT JOIN  = 取全部 orders,再补上能匹配到的 users

这也是为什么很多人说,学连接查询本质上是在学结果集保留规则


Java 开发里最常见的写法

以 MyBatis 或 JDBC 的列表查询为例,最常见的是左连接。

SELECT
    u.id,
    u.username,
    u.status,
    o.order_no,
    o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 1;

这个 SQL 的业务含义是:

  • 查询所有启用状态的用户
  • 如果用户有订单,就带出订单信息
  • 如果没有订单,用户仍然要返回

这类写法很适合后台列表页、详情页、导出报表等需求。


一个非常容易踩的坑:WHERE 条件把左连接写成了内连接

这是实际项目中最常见的问题。

先看这段 SQL:

SELECT
    u.id,
    u.username,
    o.order_no
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.amount > 100;

很多人以为这是“查询所有用户,并带出金额大于 100 的订单”,其实不是。

为什么会出问题

因为 LEFT JOIN 后,如果某个用户没有订单,那么 o.amountNULL

WHERE o.amount > 100 会把这些 NULL 行直接过滤掉,于是结果只剩下有匹配订单且金额大于 100 的用户。最终效果就接近内连接了。

正确写法

应该把右表过滤条件写到 ON 里:

SELECT
    u.id,
    u.username,
    o.order_no
FROM users u
LEFT JOIN orders o
    ON u.id = o.user_id
   AND o.amount > 100;

这样语义才是:

  • 左表用户全部保留
  • 只关联金额大于 100 的订单
  • 没有关联上的订单,右侧字段为 NULL

经验原则

如果你想保留左表所有记录,那么:

  • 左表过滤条件写 WHERE
  • 右表过滤条件优先写 ON

这条规则非常实用。


另一个常见问题:一对多关联导致结果行数变多

例如一个用户有多个订单:

SELECT
    u.id,
    u.username,
    o.order_no
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

alice 会出现两行,因为她有两个订单。

这不是 SQL 错了,而是关系本身就是一对多

解决思路

根据业务场景选择:

1. 只取一条关联记录

比如取最新订单,可以先子查询再关联。

SELECT
    u.id,
    u.username,
    t.order_no,
    t.amount
FROM users u
LEFT JOIN (
    SELECT o1.*
    FROM orders o1
    WHERE o1.id = (
        SELECT MAX(o2.id)
        FROM orders o2
        WHERE o2.user_id = o1.user_id
    )
) t ON u.id = t.user_id;

2. 聚合后再关联

比如统计订单数量:

SELECT
    u.id,
    u.username,
    IFNULL(t.order_count, 0) AS order_count
FROM users u
LEFT JOIN (
    SELECT
        user_id,
        COUNT(*) AS order_count
    FROM orders
    GROUP BY user_id
) t ON u.id = t.user_id;

在 Java 接口开发中,这种“先聚合再关联”的写法很常见,尤其适合列表统计字段。


ONWHERE 的职责不要混淆

可以这样记:

  • ON:定义两张表怎么关联,以及关联时的附加条件
  • WHERE:对关联后的结果再做过滤

举个对比:

写在 ON

LEFT JOIN orders o
    ON u.id = o.user_id
   AND o.amount > 100

含义:用户全部保留,只关联金额大于 100 的订单。

写在 WHERE

LEFT JOIN orders o ON u.id = o.user_id
WHERE o.amount > 100

含义:最终结果只保留订单金额大于 100 的行,没有订单的用户也会被过滤掉。

这两种写法在结果上可能完全不同。


实战中怎么选:一套简单判断方法

拿到需求后,可以按下面顺序判断。

场景一:只关心有关系的数据

例如:

  • 查有订单的用户
  • 查已绑定角色的账号
  • 查有库存记录的商品

用:

INNER JOIN

场景二:主表必须完整返回,关联数据可有可无

例如:

  • 查所有用户及其最近一次登录信息
  • 查所有商品及其库存信息
  • 查所有菜单及某角色的授权情况

用:

LEFT JOIN

场景三:SQL 已经写成右连接

能改成左连接就改成左连接,除非团队有特殊规范或历史原因。


在 MyBatis XML 里的一些建议

1. 永远显式写 JOIN ... ON

不要再写老式隐式连接:

SELECT *
FROM users u, orders o
WHERE u.id = o.user_id;

这种写法的问题是:

  • 连接条件和过滤条件混在一起
  • 稍微复杂一点就不容易维护
  • 漏写条件时容易产生笛卡尔积

更推荐:

SELECT *
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

2. 别滥用 SELECT *

多表关联时,字段重名非常常见,比如 idcreate_timestatus

建议明确写出字段并起别名:

SELECT
    u.id AS user_id,
    u.username,
    o.id AS order_id,
    o.order_no,
    o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

这样在 Java 实体映射、DTO 封装、Map 接收时都更清晰。

3. 动态 SQL 时更要注意条件位置

MyBatis 中很多 bug 都出在动态拼接条件时,把右表条件拼到了 WHERE 中,导致左连接失效。

错误示例:

<select id="selectUserOrders" resultType="com.demo.UserOrderDTO">
    SELECT
        u.id,
        u.username,
        o.order_no
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    <where>
        <if test="minAmount != null">
            AND o.amount <![CDATA[>=]]> #{minAmount}
        </if>
    </where>
</select>

如果 minAmount 有值,这段 SQL 的语义就偏向内连接了。

更稳妥的方式是把右表关联限制放进 ON 的动态片段里。


性能上要注意什么

连接方式不仅影响结果,也影响执行计划。

1. 关联字段要有索引

比如:

  • users.id
  • orders.user_id

尤其是被关联的外键字段,没索引时多表查询性能会明显下降。

2. 先过滤再关联,减少数据量

例如先在主表或子查询里把状态、时间范围缩小,再做连接,通常比直接大表 join 更稳。

3. 不要把“统计”和“明细”混成一个大查询

很多慢 SQL 不是因为 LEFT JOIN,而是因为:

  • 多表 join
  • 再 group by
  • 再 order by
  • 再分页

这种场景要考虑拆分查询,或者先聚合后关联。

4. 右连接不是性能优化手段

RIGHT JOINLEFT JOIN 本质是语义选择,不是性能技巧。不要指望“把左连接改成右连接”能自动变快,执行效率要看优化器、索引和数据量。


一段最容易记住的总结

写连接查询时,先别急着写 SQL,先问自己三件事:

  1. 哪张表是主表
  2. 主表的数据是否必须全部保留
  3. 关联表没有数据时,结果还能不能接受

对应关系非常直接:

  • 必须两边都匹配:INNER JOIN
  • 左边必须全部保留:LEFT JOIN
  • 右边必须全部保留:RIGHT JOIN
  • 团队开发里,优先使用 LEFT JOIN 表达主表保留逻辑

真正的难点不在于记住语法,而在于理解结果集是怎么被保留下来的。只要把“保留哪张表的数据”想清楚,连接查询基本就不会写错。

正文到此结束
评论插件初始化中...
Loading...