SQL 左连接、右连接与内连接的使用与区别
为什么连接查询一定要先分清“主表”和“保留行”
在 Java 后端开发里,业务查询很少只查一张表。用户列表要带部门名,订单列表要带用户信息,权限查询要关联角色和菜单,这些场景都离不开 JOIN。
真正容易出错的地方,不是 JOIN 语法本身,而是没有搞清楚两件事:
- 谁是主表
- 哪张表的数据必须保留下来
理解这两点后,左连接、右连接、内连接的区别就非常清楚了。
先准备一组演示数据
下面用“用户表”和“订单表”举例。
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
可以看到:
charlie和david被过滤掉了,因为他们没有订单- 订单表里如果出现没有对应用户的数据,也不会被查出来
内连接的核心特点
- 只保留关联成功的数据
- 两边任意一边缺失,结果就不会出现
- 最适合“必须有关联数据才有业务意义”的场景
常见使用场景
- 查询“有订单的用户”
- 查询“已分配角色的账号”
- 查询“存在有效明细的主单”
左连接:以左表为准,左边数据全部保留
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.amount 是 NULL。
而 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 接口开发中,这种“先聚合再关联”的写法很常见,尤其适合列表统计字段。
ON 和 WHERE 的职责不要混淆
可以这样记:
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 *
多表关联时,字段重名非常常见,比如 id、create_time、status。
建议明确写出字段并起别名:
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.idorders.user_id
尤其是被关联的外键字段,没索引时多表查询性能会明显下降。
2. 先过滤再关联,减少数据量
例如先在主表或子查询里把状态、时间范围缩小,再做连接,通常比直接大表 join 更稳。
3. 不要把“统计”和“明细”混成一个大查询
很多慢 SQL 不是因为 LEFT JOIN,而是因为:
- 多表 join
- 再 group by
- 再 order by
- 再分页
这种场景要考虑拆分查询,或者先聚合后关联。
4. 右连接不是性能优化手段
RIGHT JOIN 和 LEFT JOIN 本质是语义选择,不是性能技巧。不要指望“把左连接改成右连接”能自动变快,执行效率要看优化器、索引和数据量。
一段最容易记住的总结
写连接查询时,先别急着写 SQL,先问自己三件事:
- 哪张表是主表
- 主表的数据是否必须全部保留
- 关联表没有数据时,结果还能不能接受
对应关系非常直接:
- 必须两边都匹配:
INNER JOIN - 左边必须全部保留:
LEFT JOIN - 右边必须全部保留:
RIGHT JOIN - 团队开发里,优先使用
LEFT JOIN表达主表保留逻辑
真正的难点不在于记住语法,而在于理解结果集是怎么被保留下来的。只要把“保留哪张表的数据”想清楚,连接查询基本就不会写错。