写在前面
大家好,欢迎来到MySQL全面教学系列的第6天!昨天我们学习了聚合函数与分组查询,掌握了数据统计分析的核心技能。今天,我们将进入更加实用的领域------多表查询与JOIN。
在实际业务中,数据往往分散在多个表中。用户表、订单表、商品表、分类表......如何将这些表的数据关联起来进行查询,是每个开发者必须掌握的技能。JOIN操作是SQL中最重要、最常用的操作之一。
让我们开始今天的学习!

目录
-
- 写在前面
- 一、为什么需要多表查询
-
- [1.1 数据库范式与数据拆分](#1.1 数据库范式与数据拆分)
- [1.2 实际业务场景](#1.2 实际业务场景)
- [二、INNER JOIN内连接](#二、INNER JOIN内连接)
-
- [2.1 基本语法](#2.1 基本语法)
- [2.2 实战示例](#2.2 实战示例)
- [2.3 多表连接](#2.3 多表连接)
- [2.4 使用USING简化](#2.4 使用USING简化)
- [三、LEFT JOIN和RIGHT JOIN外连接](#三、LEFT JOIN和RIGHT JOIN外连接)
-
- [3.1 LEFT JOIN左连接](#3.1 LEFT JOIN左连接)
- [3.2 RIGHT JOIN右连接](#3.2 RIGHT JOIN右连接)
- [3.3 各种JOIN对比](#3.3 各种JOIN对比)
- [四、FULL JOIN全连接](#四、FULL JOIN全连接)
-
- [4.1 MySQL不支持FULL JOIN](#4.1 MySQL不支持FULL JOIN)
- [4.2 使用UNION ALL优化](#4.2 使用UNION ALL优化)
- [五、CROSS JOIN笛卡尔积](#五、CROSS JOIN笛卡尔积)
- 六、自连接
-
- [6.1 员工-上级关系](#6.1 员工-上级关系)
- [6.2 查询员工及其上级](#6.2 查询员工及其上级)
- [6.3 查询层级结构](#6.3 查询层级结构)
- 七、实战:电商系统多表查询
-
- [7.1 表结构](#7.1 表结构)
- [7.2 用户订单统计](#7.2 用户订单统计)
- [7.3 商品销售分析](#7.3 商品销售分析)
- [7.4 复杂业务查询](#7.4 复杂业务查询)
- 八、踩坑提醒与经验之谈
-
- [8.1 JOIN条件写错导致笛卡尔积](#8.1 JOIN条件写错导致笛卡尔积)
- [8.2 ON和WHERE的区别](#8.2 ON和WHERE的区别)
- [8.3 多表JOIN的性能优化](#8.3 多表JOIN的性能优化)
- [8.4 NULL值处理](#8.4 NULL值处理)
- 九、面试高频考点
-
- [9.1 INNER JOIN和LEFT JOIN的区别?](#9.1 INNER JOIN和LEFT JOIN的区别?)
- [9.2 如何找出没有订单的用户?](#9.2 如何找出没有订单的用户?)
- [9.3 ON和WHERE在JOIN中的区别?](#9.3 ON和WHERE在JOIN中的区别?)
- [9.4 如何优化多表JOIN查询?](#9.4 如何优化多表JOIN查询?)
- [9.5 什么是笛卡尔积?如何避免?](#9.5 什么是笛卡尔积?如何避免?)
- 十、总结
- 参考资料
- 互动话题
一、为什么需要多表查询
1.1 数据库范式与数据拆分
为了避免数据冗余和保持数据一致性,我们遵循数据库范式将数据拆分到不同的表中:
| 表名 | 存储内容 | 避免的问题 |
|---|---|---|
| users | 用户信息 | 重复存储用户信息 |
| orders | 订单信息 | 订单与用户解耦 |
| products | 商品信息 | 商品信息统一管理 |
| categories | 分类信息 | 分类信息复用 |
1.2 实际业务场景
假设我们要查询"用户张三的所有订单详情",数据分散在多个表中:
sql
-- 用户表
users(user_id, username, email)
-- 订单表
orders(order_id, user_id, order_date, total_amount)
-- 订单商品表
order_items(item_id, order_id, product_id, quantity, price)
-- 商品表
products(product_id, product_name, category_id)
这就需要多表查询来关联这些分散的数据。
二、INNER JOIN内连接
INNER JOIN返回两个表中匹配的行,是最常用的JOIN类型。
2.1 基本语法
sql
SELECT columns
FROM table1
INNER JOIN table2 ON table1.column = table2.column;
2.2 实战示例
sql
-- 查询用户及其订单信息
SELECT
u.user_id,
u.username,
u.email,
o.order_id,
o.order_date,
o.total_amount
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id;
2.3 多表连接
sql
-- 查询订单详情(用户+订单+商品)
SELECT
u.username,
o.order_id,
o.order_date,
p.product_name,
oi.quantity,
oi.price,
(oi.quantity * oi.price) AS subtotal
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id;
2.4 使用USING简化
当两个表的关联字段名称相同时,可以使用USING简化语法:
sql
-- 等同于 ON u.user_id = o.user_id
SELECT u.username, o.order_id
FROM users u
INNER JOIN orders o USING(user_id);
三、LEFT JOIN和RIGHT JOIN外连接
外连接会保留一个表的所有行,即使另一个表没有匹配的行。
3.1 LEFT JOIN左连接
保留左表的所有行,右表没有匹配的行用NULL填充。
sql
-- 查询所有用户及其订单(包括没有订单的用户)
SELECT
u.user_id,
u.username,
o.order_id,
o.total_amount
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id;
结果示例:
| user_id | username | order_id | total_amount |
|---|---|---|---|
| 1 | 张三 | 1001 | 199.00 |
| 1 | 张三 | 1002 | 299.00 |
| 2 | 李四 | NULL | NULL |
| 3 | 王五 | 1003 | 150.00 |
3.2 RIGHT JOIN右连接
保留右表的所有行,左表没有匹配的行用NULL填充。
sql
-- 查询所有订单及对应的用户(包括用户已被删除的订单)
SELECT
u.user_id,
u.username,
o.order_id,
o.total_amount
FROM users u
RIGHT JOIN orders o ON u.user_id = o.user_id;
经验之谈: 实际开发中LEFT JOIN使用频率远高于RIGHT JOIN,因为RIGHT JOIN可以通过交换表的位置转换为LEFT JOIN,更易理解。
3.3 各种JOIN对比
| JOIN类型 | 结果集 | 使用场景 |
|---|---|---|
| INNER JOIN | 只返回匹配的行 | 只关心有关联的数据 |
| LEFT JOIN | 返回左表所有行 | 需要保留左表全部数据 |
| RIGHT JOIN | 返回右表所有行 | 需要保留右表全部数据 |
四、FULL JOIN全连接
FULL JOIN返回两个表的所有行,没有匹配的行用NULL填充。
4.1 MySQL不支持FULL JOIN
MySQL不直接支持FULL JOIN语法,但可以用UNION模拟:
sql
-- 模拟FULL JOIN
SELECT u.user_id, u.username, o.order_id
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
UNION
SELECT u.user_id, u.username, o.order_id
FROM users u
RIGHT JOIN orders o ON u.user_id = o.user_id;
4.2 使用UNION ALL优化
如果确定不会有重复数据,使用UNION ALL性能更好:
sql
-- 找出所有用户和所有订单(无关联关系)
SELECT 'user' AS type, user_id AS id, username AS name FROM users
UNION ALL
SELECT 'order' AS type, order_id AS id, CAST(total_amount AS CHAR) FROM orders;
五、CROSS JOIN笛卡尔积
CROSS JOIN返回两个表的笛卡尔积,即所有可能的组合。
sql
-- 笛卡尔积(慎用!数据量会爆炸)
SELECT * FROM users CROSS JOIN orders;
-- 如果users有1000条,orders有10000条,结果将是1000万条!
实际应用场景: 生成测试数据、排列组合计算
sql
-- 生成所有可能的尺码和颜色组合
SELECT s.size, c.color
FROM sizes s
CROSS JOIN colors c;
踩坑提醒: 忘记写JOIN条件会导致隐式笛卡尔积!
sql
-- 错误!忘记ON条件,产生笛卡尔积
SELECT * FROM users u JOIN orders o; -- 危险!
-- 正确写法
SELECT * FROM users u JOIN orders o ON u.user_id = o.user_id;
六、自连接
自连接是一个表与自身的连接,常用于查询层级关系。
6.1 员工-上级关系
sql
-- 员工表(包含上级ID)
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(50),
manager_id INT,
department VARCHAR(50)
);
-- 插入数据
INSERT INTO employees VALUES
(1, '总经理', NULL, '管理层'),
(2, '技术总监', 1, '技术部'),
(3, '销售总监', 1, '销售部'),
(4, '开发组长', 2, '技术部'),
(5, '开发工程师', 4, '技术部');
6.2 查询员工及其上级
sql
-- 查询每个员工及其上级
SELECT
e.emp_id,
e.emp_name AS employee,
m.emp_name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;
结果:
| emp_id | employee | manager |
|---|---|---|
| 1 | 总经理 | NULL |
| 2 | 技术总监 | 总经理 |
| 3 | 销售总监 | 总经理 |
| 4 | 开发组长 | 技术总监 |
| 5 | 开发工程师 | 开发组长 |
6.3 查询层级结构
sql
-- 查询某员工的所有下级
WITH RECURSIVE subordinates AS (
-- 基准:从指定员工开始
SELECT emp_id, emp_name, manager_id, 0 AS level
FROM employees
WHERE emp_id = 2 -- 技术总监
UNION ALL
-- 递归:查找下级
SELECT e.emp_id, e.emp_name, e.manager_id, s.level + 1
FROM employees e
INNER JOIN subordinates s ON e.manager_id = s.emp_id
)
SELECT * FROM subordinates;
七、实战:电商系统多表查询
7.1 表结构
sql
-- 用户表
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
city VARCHAR(50),
register_date DATE
);
-- 商品分类表
CREATE TABLE categories (
category_id INT PRIMARY KEY,
category_name VARCHAR(50),
parent_id INT
);
-- 商品表
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100),
category_id INT,
price DECIMAL(10,2),
stock INT
);
-- 订单表
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT,
order_status VARCHAR(20),
total_amount DECIMAL(10,2),
create_time DATETIME
);
-- 订单商品表
CREATE TABLE order_items (
item_id INT PRIMARY KEY,
order_id INT,
product_id INT,
quantity INT,
unit_price DECIMAL(10,2)
);
7.2 用户订单统计
sql
-- 查询用户的订单统计信息
SELECT
u.user_id,
u.username,
u.city,
COUNT(DISTINCT o.order_id) AS order_count,
COALESCE(SUM(o.total_amount), 0) AS total_spent,
COALESCE(AVG(o.total_amount), 0) AS avg_order_amount,
MAX(o.create_time) AS last_order_time
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
AND o.order_status != 'cancelled'
GROUP BY u.user_id, u.username, u.city;
7.3 商品销售分析
sql
-- 各类别商品销售统计
SELECT
c.category_name,
COUNT(DISTINCT p.product_id) AS product_count,
COUNT(oi.item_id) AS sold_count,
SUM(oi.quantity) AS total_quantity,
SUM(oi.quantity * oi.unit_price) AS total_revenue
FROM categories c
LEFT JOIN products p ON c.category_id = p.category_id
LEFT JOIN order_items oi ON p.product_id = oi.product_id
LEFT JOIN orders o ON oi.order_id = o.order_id
AND o.order_status = 'completed'
GROUP BY c.category_id, c.category_name
ORDER BY total_revenue DESC;
7.4 复杂业务查询
sql
-- 查询购买了特定商品的用户列表
SELECT DISTINCT
u.user_id,
u.username,
u.email,
o.order_id,
o.create_time
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
WHERE oi.product_id = 1001
AND o.order_status = 'completed'
ORDER BY o.create_time DESC;
-- 查询购买了"手机"类别商品的用户的其他购买记录
SELECT DISTINCT
u.username,
p2.product_name,
c.category_name
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
INNER JOIN categories c ON p.category_id = c.category_id
INNER JOIN order_items oi2 ON o.order_id = oi2.order_id
INNER JOIN products p2 ON oi2.product_id = p2.product_id
INNER JOIN categories c2 ON p2.category_id = c2.category_id
WHERE c.category_name = '手机'
AND c2.category_name != '手机';
八、踩坑提醒与经验之谈
8.1 JOIN条件写错导致笛卡尔积
错误示例:
sql
-- 危险!忘记ON条件
SELECT * FROM users u
JOIN orders o; -- 产生笛卡尔积!
-- 危险!关联条件错误
SELECT * FROM users u
JOIN orders o ON u.user_id = o.order_id; -- 关联字段错误!
经验之谈: 写JOIN时,先写ON条件,再写SELECT字段。多表JOIN时,建议一次只加一个表,逐步验证结果。
8.2 ON和WHERE的区别
sql
-- LEFT JOIN + ON条件:保留左表所有行
SELECT u.username, o.order_id, o.total_amount
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
AND o.order_date > '2024-01-01'; -- ON中的条件不影响左表
-- LEFT JOIN + WHERE条件:过滤最终结果
SELECT u.username, o.order_id, o.total_amount
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_date > '2024-01-01'; -- WHERE会过滤掉NULL行
关键区别:
- ON条件:在JOIN时过滤右表数据,不影响左表
- WHERE条件:在JOIN完成后过滤整个结果集
8.3 多表JOIN的性能优化
sql
-- 低效写法:大表在前
SELECT * FROM big_table b
JOIN small_table s ON b.id = s.id;
-- 高效写法:小表在前(MySQL优化器通常会处理,但显式指定更好)
SELECT * FROM small_table s
JOIN big_table b ON s.id = b.id;
-- 确保关联字段有索引
CREATE INDEX idx_user_id ON orders(user_id);
8.4 NULL值处理
sql
-- 使用COALESCE处理NULL
SELECT
u.username,
COALESCE(SUM(o.total_amount), 0) AS total_spent
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.username;
-- 过滤NULL值
SELECT u.username, o.order_id
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NOT NULL; -- 只保留有订单的用户
九、面试高频考点
9.1 INNER JOIN和LEFT JOIN的区别?
答案:
- INNER JOIN只返回两个表中匹配的行
- LEFT JOIN返回左表的所有行,右表没有匹配的行用NULL填充
- 使用场景:只关心有关联的数据用INNER JOIN;需要保留主表全部数据用LEFT JOIN
9.2 如何找出没有订单的用户?
答案: 使用LEFT JOIN + IS NULL
sql
SELECT u.user_id, u.username
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;
或者使用NOT EXISTS:
sql
SELECT u.user_id, u.username
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.user_id
);
9.3 ON和WHERE在JOIN中的区别?
答案:
- ON:定义JOIN的匹配条件,决定哪些行可以关联
- WHERE:在JOIN完成后过滤结果集
- 对于OUTER JOIN,ON中的条件不会过滤主表的行,WHERE会
9.4 如何优化多表JOIN查询?
答案:
- 确保关联字段有索引
- 小表驱动大表
- 只SELECT需要的字段,避免SELECT *
- 使用EXPLAIN分析执行计划
- 考虑反范式设计,适当冗余字段
9.5 什么是笛卡尔积?如何避免?
答案: 笛卡尔积是两个表所有行的组合,数据量会爆炸式增长。避免方法:
- 确保JOIN语句有ON条件
- 检查关联条件是否正确
- 使用EXPLAIN检查执行计划
十、总结
今天我们学习了MySQL多表查询与JOIN的核心知识:
- INNER JOIN:内连接,只返回匹配的行,最常用
- LEFT/RIGHT JOIN:外连接,保留一侧表的所有行
- FULL JOIN:MySQL不支持,用UNION模拟
- CROSS JOIN:笛卡尔积,慎用
- 自连接:表与自身的连接,用于层级关系查询
- 实战应用:电商系统的多表关联查询
下一步预告
Day7:MySQL子查询与高级查询
明天我们将学习更高级的查询技巧------子查询、窗口函数和CTE。这些功能强大的工具将帮助你解决更复杂的业务查询需求。子查询可以嵌套在其他查询中,窗口函数可以进行复杂的分析计算,CTE可以让你的SQL更加清晰易读。敬请期待!
参考资料
MySQL 8.0 Reference Manual - JOIN Syntax
互动话题
- 你在使用JOIN时遇到过哪些性能问题?是如何解决的?
- 你更喜欢使用JOIN还是子查询?为什么?
- 在实际项目中,你遇到过哪些复杂的JOIN场景?
如果觉得本文对你有帮助,请点赞收藏!明天见!