【MySQL全面教学】MySQL多表查询与JOIN Day6(2026年)

写在前面

大家好,欢迎来到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查询?

答案:

  1. 确保关联字段有索引
  2. 小表驱动大表
  3. 只SELECT需要的字段,避免SELECT *
  4. 使用EXPLAIN分析执行计划
  5. 考虑反范式设计,适当冗余字段

9.5 什么是笛卡尔积?如何避免?

答案: 笛卡尔积是两个表所有行的组合,数据量会爆炸式增长。避免方法:

  1. 确保JOIN语句有ON条件
  2. 检查关联条件是否正确
  3. 使用EXPLAIN检查执行计划

十、总结

今天我们学习了MySQL多表查询与JOIN的核心知识:

  1. INNER JOIN:内连接,只返回匹配的行,最常用
  2. LEFT/RIGHT JOIN:外连接,保留一侧表的所有行
  3. FULL JOIN:MySQL不支持,用UNION模拟
  4. CROSS JOIN:笛卡尔积,慎用
  5. 自连接:表与自身的连接,用于层级关系查询
  6. 实战应用:电商系统的多表关联查询

下一步预告

Day7:MySQL子查询与高级查询

明天我们将学习更高级的查询技巧------子查询、窗口函数和CTE。这些功能强大的工具将帮助你解决更复杂的业务查询需求。子查询可以嵌套在其他查询中,窗口函数可以进行复杂的分析计算,CTE可以让你的SQL更加清晰易读。敬请期待!


参考资料

MySQL 8.0 Reference Manual - JOIN Syntax


互动话题

  1. 你在使用JOIN时遇到过哪些性能问题?是如何解决的?
  2. 你更喜欢使用JOIN还是子查询?为什么?
  3. 在实际项目中,你遇到过哪些复杂的JOIN场景?

如果觉得本文对你有帮助,请点赞收藏!明天见!

相关推荐
倒流时光三十年7 小时前
PostgreSQL COPY命令:高效数据导入的最佳实践
数据库·postgresql
Full Stack Developme7 小时前
Spring Boot 状态机 与 com.alibaba.cola 中的状态机
java·spring boot·后端
shuair7 小时前
redis分布式锁
数据库·redis·分布式
MacroZheng7 小时前
让 Claude Code 成本爆降 89%,这个开源工具有点猛...
java·人工智能·后端
_Evan_Yao7 小时前
游戏和编程两不误:用Unity做一个简单小游戏
后端·游戏·unity·游戏引擎
咕噜咕噜啦啦7 小时前
从spring到spring boot——JAVA项目开发
java·前端·spring boot·后端·spring
Gopher_HBo8 小时前
DNS和HTTP DNS
后端
今天背单词了吗9808 小时前
MySQL InnoDB引擎八大核心特性详解(高频面试题)
java·数据库·mysql
麦聪聊数据8 小时前
中小企无需重型数据中台:轻量化数据体系搭建完整方案
数据库