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

在关系型数据库的实际开发过程中,表之间的关联查询是日常操作中最常见的部分之一。随着系统复杂度提升,表结构之间的关联越来越多,如何正确使用不同类型的连接语句直接影响查询性能、数据返回结果的准确性以及业务逻辑的清晰度。掌握左连接、右连接和内连接的使用方法,对于构建可靠的数据访问层至关重要。

基本概念与语法定义

关联查询的核心思想是把两张(或多张)表按照字段建立映射关系。无论使用 MySQL、PostgreSQL、SQL Server,SQL92 标准后的 JOIN 语法基本一致。

内连接(INNER JOIN)

内连接只返回两张表都能匹配的记录。如果一条记录在任意一张表中无法匹配,则不会出现在结果集中。

语法:

SELECT *
FROM A
INNER JOIN B ON A.id = B.a_id;

左连接(LEFT JOIN / LEFT OUTER JOIN)

左连接返回左表的所有记录,即使右表中没有匹配,也会出现,右表字段为 NULL

语法:

SELECT *
FROM A
LEFT JOIN B ON A.id = B.a_id;

右连接(RIGHT JOIN / RIGHT OUTER JOIN)

右连接返回右表的所有记录,即使左表没有匹配,也会被返回,左表字段为 NULL

语法:

SELECT *
FROM A
RIGHT JOIN B ON A.id = B.a_id;

建表 SQL 示例

下面使用两张简单的表展开说明:用户表与订单表。

CREATE TABLE user_info (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100)
);

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    amount DECIMAL(10,2),
    create_time DATETIME,
    FOREIGN KEY (user_id) REFERENCES user_info(id)
);

示例数据:

INSERT INTO user_info (id, username, email) VALUES
(1, 'Alice', 'alice@test.com'),
(2, 'Bob', 'bob@test.com'),
(3, 'Cindy', 'cindy@test.com');

INSERT INTO orders (id, user_id, amount, create_time) VALUES
(101, 1, 200.00, '2024-01-01 10:00:00'),
(102, 1, 300.00, '2024-01-02 15:00:00'),
(103, 2, 150.00, '2024-01-03 20:00:00');

此时 Alice 有 2 单订单,Bob 有 1 单订单,Cindy 没有订单。

以下操作将基于这组数据进行演示。

内连接实际效果

执行:

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

返回内容仅包含 A、B 两张表都能匹配的记录。

返回结果:

  • Alice – 200.00
  • Alice – 300.00
  • Bob – 150.00

Cindy 没有出现,因为她没有订单。

内连接的适用场景:

  • 查询两张表都必须存在的数据,例如:查询有订单的用户
  • 过滤掉业务中“没有关联记录”的情况

在业务逻辑上,内连接通常意味着“强耦合”关系,例如订单必然属于一个用户。

左连接实际效果

执行:

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

返回结果包含所有用户:

  • Alice – 200.00
  • Alice – 300.00
  • Bob – 150.00
  • Cindy – NULL(无订单)

左连接的应用场景:

  • 希望“主表”的信息全部展示,副表仅作补充
  • 查询所有用户及其订单数量,即使某些用户没有下单
  • 给报表补全信息(例如统计转化率)

左连接通常表示业务上的主从结构,主表数据永远需要展示,关联表只是附属信息。

右连接实际效果

执行:

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

仅当右表记录全部展示时才使用右连接。

返回结果:

  • Alice – 200.00
  • Alice – 300.00
  • Bob – 150.00

右连接应用较少,因为通常业务核心数据在左表,因此左连接更符合直观逻辑。

右连接主要用于:

  • 右表为主数据,必须全部显示
  • 一些复杂 SQL 中,右连接可以减少子查询复杂度,但本质上与左连接等价

三种 JOIN 的核心区别总结

下面用表格整理三种连接方式的差异:

JOIN 类型 是否需要两张表都匹配 是否保留左表全部数据 是否保留右表全部数据 Cindy 是否出现 使用场景
INNER JOIN 查询双方必须匹配的记录
LEFT JOIN 主表必显,关联表可无
RIGHT JOIN 辅表主导查询时使用

重要结论:

  • 左连接与右连接只是方向不同,可以互换:

    A LEFT JOIN B  ≡  B RIGHT JOIN A
    
  • 内连接总是返回交集

  • 左/右连接返回并集的一部分,但保留主表信息

SQL 执行顺序与 JOIN 的关系

理解 SQL 的执行顺序有助于分析复杂 JOIN:

  1. FROM 左表
  2. JOIN 右表
  3. ON 条件过滤
  4. WHERE 条件过滤
  5. GROUP BY 聚合
  6. HAVING 聚合过滤
  7. SELECT 字段
  8. ORDER BY 排序
  9. LIMIT 分页

重点:WHERE 会过滤 NULL 记录,导致左联效果被削弱。

示例:

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

此时 Cindy 会被过滤,因为 o.amountNULL

如果想保留 Cindy,则必须把条件放在 ON 中:

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

Cindy 保留,但 amount 为 NULL。

在业务开发中,经常因为 WHERE 写错位置导致联表结果与预期不符。

多表 JOIN 查询设计

复杂系统中一个页面可能需要十几个字段来自不同表。合理设计 JOIN 顺序会影响性能。

常见建议:

  1. 驱动表(主表)放在 FROM 左侧
  2. 辅助数据表放 JOIN 后
  3. 使用 LEFT JOIN 适配缺省信息
  4. 尽量避免多层 RIGHT JOIN,逻辑不直观
  5. ON 中建立合适的索引字段

例如:

SELECT 
    u.username,
    o.amount,
    p.product_name
FROM user_info u
LEFT JOIN orders o ON u.id = o.user_id
LEFT JOIN product p ON o.product_id = p.id;

通过逐层左连接保证用户信息永远存在,同时补充订单与商品信息。

JOIN 性能优化策略

JOIN 操作可能导致全表扫描,性能瓶颈突出。优化方法包括:

建立适配索引

JOIN 使用的字段必须建索引。

例如:

ALTER TABLE orders ADD INDEX idx_user (user_id);

避免发生 Using temporary 或 Using filesort。

避免使用隐式类型转换

例如一个字段是 VARCHAR,另一个是 INT,会导致索引失效。

小表驱动大表

MySQL 优化器通常选择小表作为驱动表,如:

SELECT *
FROM big_table b
INNER JOIN small_table s ON b.id = s.id;

如果 small_table 过滤更强,应主动调整:

SELECT *
FROM small_table s
INNER JOIN big_table b ON b.id = s.id;

避免过多 JOIN

超过 5 个以上 JOIN 通常应考虑:

  • 拆分业务查询
  • 预计算冗余字段
  • 将部分数据缓存至 Redis
  • 使用视图或物化视图结构化数据

在 Java / MyBatis / JPA 中的实践

框架层对 JOIN 的封装更加贴近业务需求。

MyBatis 示例

<select id="selectUserOrders" resultType="UserOrderDTO">
    SELECT u.username, o.amount
    FROM user_info u
    LEFT JOIN orders o ON u.id = o.user_id
</select>

JPA 示例

@Query("SELECT new com.demo.UserOrderDTO(u.username, o.amount) " +
       "FROM User u LEFT JOIN u.orders o")
List<UserOrderDTO> list();

Spring Data JPA 的 JOIN Fetch

解决 N+1 查询:

@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders")
List<User> findWithOrders();

JOIN 错误与常见排查方法

结果数异常翻倍

通常原因:

  • 一对多关系未聚合,导致重复行

解决办法:

  • 使用 DISTINCT
  • 或者聚合 GROUP BY u.id

WHERE 导致 NULL 行丢失

将条件放到 ON 里。

混淆 LEFT JOIN 与 INNER JOIN

若补全主表数据,则必须用 LEFT JOIN。

JOIN 字段无索引导致极慢

使用 EXPLAIN 检查执行计划:

EXPLAIN SELECT ...

若 type=ALL,则索引未生效。

JOIN 与子查询的取舍

JOIN 与子查询逻辑类似,但性能不完全相同:

  • JOIN 更适合关联多字段、复杂关系
  • 子查询适合过滤主表记录
  • MySQL 8.0 后优化器提升,子查询性能接近 JOIN

示例对比:

JOIN

SELECT u.*
FROM user_info u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.amount > 200;

子查询

SELECT *
FROM user_info
WHERE id IN (SELECT user_id FROM orders WHERE amount > 200);

JOIN 强调关联信息展示,子查询偏向过滤。

实战场景:统计用户订单金额

需求:列出所有用户、订单总金额、订单数量。

SELECT 
    u.id,
    u.username,
    COUNT(o.id) AS order_count,
    SUM(o.amount) AS total_amount
FROM user_info u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.username;

Cindy 会展示,但订单数为 0。

实战场景:筛选最近一个月内有下单的用户

SELECT DISTINCT u.*
FROM user_info u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY);

使用内连接过滤无关用户。

实战场景:统计未下单用户

SELECT u.*
FROM user_info u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

结果:只有 Cindy。

JOIN 的关键在于理解 NULL 的意义。

实战场景:同时显示用户信息与最近一条订单

需要注意不能简单 LEFT JOIN,否则会返回多条记录。

方法一:子查询

SELECT 
    u.username,
    o.amount
FROM user_info u
LEFT JOIN orders o ON o.id = (
    SELECT id FROM orders WHERE user_id = u.id ORDER BY create_time DESC LIMIT 1
);

方法二:使用窗口函数(MySQL 8.0+)

WITH cte AS (
    SELECT 
        o.*, 
        ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY create_time DESC) AS rn
    FROM orders o
)
SELECT u.username, c.amount
FROM user_info u
LEFT JOIN cte c ON u.id = c.user_id AND c.rn = 1;

JOIN 的思考方式总结

使用 JOIN 的核心判断:

  1. 主体输出是谁?放左边 → LEFT JOIN
  2. 是否必须匹配?必须 → INNER JOIN
  3. 需要补全吗?需要 → LEFT JOIN
  4. WHERE 条件是否会使左联失效?注意 NULL
  5. 是否需要聚合?使用 GROUP BY

正确使用 JOIN,不仅影响 SQL 正确性,更决定性能表现。

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