跳到主要内容

JOIN 连接

JOIN 用于根据两个或多个表之间的关系,从这些表中查询数据。本章将详细介绍各种连接类型。

为什么需要 JOIN?

关系型数据库将数据分散在多个表中,通过外键关联。JOIN 让我们能够将这些分散的数据组合起来。

-- 用户表
-- | id | name |
-- |----|-------|
-- | 1 | 张三 |
-- | 2 | 李四 |

-- 订单表
-- | id | user_id | product |
-- |----|---------|---------|
-- | 1 | 1 | 手机 |
-- | 2 | 1 | 电脑 |
-- | 3 | 2 | 平板 |

-- 使用 JOIN 查询用户及其订单
SELECT users.name, orders.product
FROM users
JOIN orders ON users.id = orders.user_id;
-- | name | product |
-- |------|---------|
-- | 张三 | 手机 |
-- | 张三 | 电脑 |
-- | 李四 | 平板 |

INNER JOIN(内连接)

内连接只返回两个表中匹配的行:

SELECT columns
FROM table1
INNER JOIN table2 ON table1.column = table2.column;

-- 示例
SELECT users.name, orders.product
FROM users
INNER JOIN orders ON users.id = orders.user_id;

特点:

  • 只返回匹配的行
  • 不匹配的行被排除
  • 最常用的连接类型

LEFT JOIN(左连接)

左连接返回左表的所有行,右表没有匹配则返回 NULL:

SELECT columns
FROM table1
LEFT JOIN table2 ON table1.column = table2.column;

-- 示例:查询所有用户及其订单(包括没有订单的用户)
SELECT users.name, orders.product
FROM users
LEFT JOIN orders ON users.id = orders.user_id;

-- 结果
-- | name | product |
-- |------|---------|
-- | 张三 | 手机 |
-- | 张三 | 电脑 |
-- | 李四 | 平板 |
-- | 王五 | NULL | <- 没有订单的用户

查找不匹配的行

-- 查找没有订单的用户
SELECT users.name
FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.id IS NULL;

RIGHT JOIN(右连接)

右连接返回右表的所有行,左表没有匹配则返回 NULL:

SELECT columns
FROM table1
RIGHT JOIN table2 ON table1.column = table2.column;

-- 示例
SELECT users.name, orders.product
FROM users
RIGHT JOIN orders ON users.id = orders.user_id;
提示

右连接可以用左连接替代,只需交换表的位置。大多数开发者更习惯使用左连接。

FULL OUTER JOIN(全外连接)

全外连接返回两个表的所有行,没有匹配则返回 NULL:

-- PostgreSQL / Oracle / SQL Server
SELECT columns
FROM table1
FULL OUTER JOIN table2 ON table1.column = table2.column;

-- MySQL 不支持 FULL OUTER JOIN,使用 UNION 模拟
SELECT columns FROM table1 LEFT JOIN table2 ON ...
UNION
SELECT columns FROM table1 RIGHT JOIN table2 ON ...;

CROSS JOIN(交叉连接)

交叉连接返回两个表的笛卡尔积(所有可能的组合):

SELECT columns
FROM table1
CROSS JOIN table2;

-- 示例:生成所有可能的组合
SELECT sizes.size, colors.color
FROM sizes
CROSS JOIN colors;

-- 如果 sizes 有 3 行,colors 有 4 行
-- 结果有 3 × 4 = 12 行

自连接

表与自身连接,用于处理层级数据:

-- 员工表包含上级 ID
-- | id | name | manager_id |
-- |----|-------|------------|
-- | 1 | 张三 | NULL |
-- | 2 | 李四 | 1 |
-- | 3 | 王五 | 1 |

-- 查询员工及其上级
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

-- 结果
-- | employee | manager |
-- |----------|---------|
-- | 张三 | NULL |
-- | 李四 | 张三 |
-- | 王五 | 张三 |

多表连接

可以连接多个表:

SELECT 
users.name,
orders.order_date,
products.product_name
FROM users
JOIN orders ON users.id = orders.user_id
JOIN order_items ON orders.id = order_items.order_id
JOIN products ON order_items.product_id = products.id;

连接顺序

-- 从左到右依次连接
FROM table1
JOIN table2 ON ...
JOIN table3 ON ...
JOIN table4 ON ...

-- 使用括号控制顺序
FROM table1
JOIN (table2 JOIN table3 ON table2.id = table3.t2_id)
ON table1.id = table2.t1_id

连接条件

使用 ON 子句

-- 单条件
JOIN table2 ON table1.id = table2.id

-- 多条件
JOIN table2 ON table1.id = table2.id AND table2.status = 'active'

使用 USING 子句

当连接列名相同时:

-- 使用 ON
JOIN orders ON users.id = orders.user_id

-- 使用 USING(列名相同)
JOIN orders USING (user_id)

-- 多列
JOIN order_items USING (order_id, product_id)

自然连接

自动匹配同名列:

-- 自动连接所有同名列
SELECT * FROM users NATURAL JOIN orders;
注意

自然连接不推荐使用,因为:

  • 隐式行为难以理解
  • 表结构变化可能导致意外结果
  • 建议显式使用 ON 或 USING

连接与过滤

ON vs WHERE

-- ON:连接条件,影响连接结果
-- WHERE:过滤条件,在连接后过滤

-- 左连接 + ON 条件
SELECT * FROM users
LEFT JOIN orders ON users.id = orders.user_id AND orders.status = 'completed';
-- 返回所有用户,没有完成订单的用户订单列为 NULL

-- 左连接 + WHERE 条件
SELECT * FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.status = 'completed';
-- 只返回有完成订单的用户(变成了内连接效果)

正确的过滤方式

-- 查询所有用户及其完成订单(包括没有完成订单的用户)
SELECT u.name, o.product
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'completed';

-- 查询只有完成订单的用户
SELECT u.name, o.product
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed';

复杂连接示例

统计每个用户的订单数

SELECT 
users.name,
COUNT(orders.id) AS order_count
FROM users
LEFT JOIN orders ON users.id = orders.user_id
GROUP BY users.id, users.name;

查找没有关联记录的行

-- 没有订单的用户
SELECT users.name
FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.id IS NULL;

-- 没有购买过某产品的用户
SELECT users.name
FROM users
LEFT JOIN orders ON users.id = orders.user_id
LEFT JOIN order_items ON orders.id = order_items.order_id
AND order_items.product_id = 123
WHERE order_items.id IS NULL;

性能优化

使用索引

-- 确保连接列有索引
CREATE INDEX idx_user_id ON orders(user_id);
CREATE INDEX idx_order_id ON order_items(order_id);

选择合适的连接类型

-- 如果只需要匹配的行,使用 INNER JOIN
SELECT * FROM users
INNER JOIN orders ON users.id = orders.user_id;

-- 如果需要保留所有左表行,使用 LEFT JOIN
SELECT * FROM users
LEFT JOIN orders ON users.id = orders.user_id;

减少连接的表数量

-- 避免:连接太多表
SELECT * FROM a
JOIN b ON ...
JOIN c ON ...
JOIN d ON ...
JOIN e ON ...
JOIN f ON ...;

-- 考虑:拆分查询或使用子查询

小结

本章我们学习了:

  1. INNER JOIN(内连接)
  2. LEFT JOIN(左连接)
  3. RIGHT JOIN(右连接)
  4. FULL OUTER JOIN(全外连接)
  5. CROSS JOIN(交叉连接)
  6. 自连接
  7. 多表连接
  8. 连接条件(ON、USING)
  9. 连接与过滤的区别
  10. 性能优化

练习

  1. 查询所有用户及其订单信息
  2. 查询没有订单的用户
  3. 查询每个用户的订单总数
  4. 使用自连接查询员工及其上级
  5. 查询购买了特定产品的用户