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 ...;
-- 考虑:拆分查询或使用子查询
小结
本章我们学习了:
- INNER JOIN(内连接)
- LEFT JOIN(左连接)
- RIGHT JOIN(右连接)
- FULL OUTER JOIN(全外连接)
- CROSS JOIN(交叉连接)
- 自连接
- 多表连接
- 连接条件(ON、USING)
- 连接与过滤的区别
- 性能优化
练习
- 查询所有用户及其订单信息
- 查询没有订单的用户
- 查询每个用户的订单总数
- 使用自连接查询员工及其上级
- 查询购买了特定产品的用户