第六章:MySQL DQL 表之间的关系 自连接 一对一、一对多、多对一、多对多

自连接通常用于处理具有层次结构递归关系的数据,比如:

  • 员工和其上级经理(都在同一张员工表中)
  • 分类目录中的父子分类
  • 社交网络中的好友关系(如"谁关注了谁")
  • 路径或层级数据(如组织架构)

语法结构

复制代码
SELECT ...
FROM table_name AS alias1
JOIN table_name AS alias2
  ON alias1.column = alias2.column;

关键点:

  • 必须使用 别名(Alias) 区分同一个表的不同实例。
  • 连接条件(ON)通常基于表中的某个外键字段(如 manager_id 引用 employee_id)。

经典示例:员工-经理关系

假设有一张员工表 employees

employee_id name manager_id
1 Alice NULL
2 Bob 1
3 Charlie 1
4 David 2

其中 manager_id 指向同一表中的 employee_id

查询每个员工及其经理的名字:

复制代码
SELECT 
  e.name AS employee,
  m.name AS manager
FROM employees AS e
LEFT JOIN employees AS m
  ON e.manager_id = m.employee_id;

结果:

employee manager
Alice NULL
Bob Alice
Charlie Alice
David Bob

使用 LEFT JOIN 是为了保留没有经理的顶级员工(如 Alice)。


注意事项

  1. 必须使用别名:否则 SQL 解析器无法区分两个"相同"的表。
  2. 避免笛卡尔积:如果没有正确的 ON 条件,自连接会产生所有行的组合(N×N),性能极差。
  3. 性能考虑 :对于深层递归(如多级上下级),自连接可能效率不高,此时可考虑:
    • 递归 CTE(Common Table Expression,支持标准 SQL 的数据库如 PostgreSQL、SQL Server、MySQL 8.0+)
    • 路径枚举(Path Enumeration)
    • 嵌套集模型(Nested Set Model)

四种关系详解

📌 注意

  • "一对多" 和 "多对一" 是同一关系的两种视角(如:一个部门有多个员工 ⇄ 一个员工属于一个部门)
  • 真正独立的关系类型只有三种:一对一、一对多、多对多

1. 一对多(One-to-Many) / 多对一(Many-to-One)

🔹 本质
  • 一方多方
  • 外键放在"多"的一方
🔹 设计原则

"多"的表中增加一个外键,指向"一"的表的主键

🔹 业务场景
  • 一个用户 → 多个订单(用户 : 订单 = 1 : N)
  • 一个部门 → 多个员工(部门 : 员工 = 1 : N)
  • 一篇文章 → 多条评论(文章 : 评论 = 1 : N)
🔹 表结构示例
复制代码
-- 一方:用户表
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL
);

-- 多方:订单表(外键在"多"的一方)
CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(32) NOT NULL,
    user_id INT NOT NULL,  -- 外键字段
    amount DECIMAL(10,2),
    
    -- 声明外键约束(推荐)
    FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE CASCADE   -- 用户删除时,自动删除其订单
        ON UPDATE CASCADE   -- 用户ID更新时,同步更新
);
🔹 查询示例
复制代码
-- 查用户及其订单
SELECT u.username, o.order_no, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id;
✅ 优点
  • 结构简单,查询高效
  • 外键保证数据完整性

2. 一对一(One-to-One)

🔹 本质
  • A 表每行 ↔ B 表最多一行
  • 通常用于拆分大表权限隔离
🔹 设计方式(两种)
方式①:外键 + 唯一约束(推荐)

在"附属表"中设置外键,并加 UNIQUE 约束

复制代码
-- 主表:用户基本信息
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50)
);

-- 附属表:用户隐私信息(一对一)
CREATE TABLE user_profiles (
    id INT PRIMARY KEY,
    user_id INT UNIQUE NOT NULL,  -- 关键:UNIQUE + 外键
    id_card VARCHAR(18),
    phone VARCHAR(11),
    
    FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE CASCADE
);

💡 user_id 既是外键,又是唯一键 → 保证一对一

方式②:主键即外键(更严格)

附属表的主键 = 主表的主键

复制代码
CREATE TABLE user_profiles (
    user_id INT PRIMARY KEY,  -- 主键 = 外键
    id_card VARCHAR(18),
    FOREIGN KEY (user_id) REFERENCES users(id)
);
🔹 业务场景
  • 用户基本信息 vs 用户隐私信息(如身份证、手机号)
  • 商品基本信息 vs 商品详细描述(避免大字段影响主表性能)
  • 用户账号 vs 用户登录凭证(安全隔离)
❗ 注意
  • 一对一不是必须拆表!只有当:
    • 字段使用频率差异大(如隐私信息很少查)
    • 表太大影响性能
    • 权限控制需要(如 DBA 能看用户表,但不能看隐私表)

3. 多对多(Many-to-Many)

🔹 本质
  • A 表多行 ↔ B 表多行
  • 必须通过中间表(关联表)实现
🔹 设计原则

创建一张中间表,包含两个外键,分别指向两端主键

🔹 业务场景
  • 学生 ↔ 课程(一个学生选多门课,一门课被多个学生选)
  • 用户 ↔ 角色(RBAC 权限模型)
  • 文章 ↔ 标签(一篇文有多标签,一个标签用于多文章)
🔹 表结构示例
复制代码
-- 学生表
CREATE TABLE students (
    id INT PRIMARY KEY,
    name VARCHAR(50)
);

-- 课程表
CREATE TABLE courses (
    id INT PRIMARY KEY,
    title VARCHAR(100)
);

-- 中间表:选课记录(无主键?不!应有复合主键或自增ID)
CREATE TABLE enrollments (
    student_id INT NOT NULL,
    course_id INT NOT NULL,
    
    -- 可选:添加时间等属性
    enrolled_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    
    -- 复合主键(推荐):防止重复选课
    PRIMARY KEY (student_id, course_id),
    
    -- 外键约束
    FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
    FOREIGN KEY (course_id)  REFERENCES courses(id)  ON DELETE CASCADE
);

💡 中间表也可有自增主键(如 id),但必须保留 (student_id, course_id) 的唯一索引以防重复。

🔹 查询示例
复制代码
-- 查学生及其所选课程
SELECT s.name, c.title
FROM students s
JOIN enrollments e ON s.id = e.student_id
JOIN courses c ON e.course_id = c.id;
✅ 优点
  • 灵活支持任意多对多关系
  • 中间表可携带额外属性(如选课时间、角色权限等级)

关系对比总览

关系类型 外键位置 是否需要中间表 典型场景
一对一 附属表(加 UNIQUE) ❌ 否 用户基本信息 vs 隐私信息
一对多 "多"的一方 ❌ 否 用户 vs 订单、部门 vs 员工
多对多 中间表(两个外键) ✅ 是 学生 vs 课程、文章 vs 标签

常见误区与最佳实践

❌ 误区1:多对多不用中间表,直接存逗号分隔 ID

复制代码
-- 错误示范!违反第一范式(1NF)
CREATE TABLE articles (
    id INT,
    tags VARCHAR(255)  -- "1,3,5" ← 不可取!
);

⚠️ 后果:

  • 无法建立外键约束
  • 无法高效查询"包含标签3的文章"
  • 数据冗余、更新困难

✅ 正确:用中间表!


❌ 误区2:外键只靠应用层维护,数据库不设约束

虽然能提升写入性能,但极易导致脏数据(如订单指向不存在的用户)。

✅ 建议:

  • 开发/测试环境:开启外键约束
  • 高并发生产环境:若性能敏感,可考虑关闭外键,但应用层必须严格校验

✅ 最佳实践清单

场景 建议
一对多 外键放"多"方,加索引
一对一 UNIQUE 外键,按需拆表
多对多 必须用中间表,设复合主键
外键命名 统一规范,如 fk_orders_user_id
删除行为 明确 ON DELETE CASCADERESTRICT
查询优化 在外键字段上建索引(InnoDB 自动为外键建索引,但显式声明更清晰)

ER 图表示法(辅助理解)

复制代码
一对一:   [User] ------(0..1)------ [Profile]
一对多:   [User] ------(1)------< [Order]
多对多:   [Student] ------(N)------< [Enrollment] >------(N)------ [Course]

总结

🌟 记住三句话

  1. "多"靠"一":一对多,外键在多的一方;
  2. "一对一,拆表加唯一":用 UNIQUE 外键保证一对一;
  3. "多对多,中间表来凑":永远不要用逗号分隔!

范式层级详解(1NF → 5NF)

📌 前提 :所有表必须是关系表(即二维表,行列清晰,无重复组)


第一范式(1NF):字段不可再分

✅ 核心要求
  • 表的每一列都是原子值(不可再分)
  • 禁止重复组、数组、JSON 对象(在传统关系模型中)
❌ 反例
复制代码
| user_id | name  | phones               |
|---------|-------|----------------------|
| 1       | 张三  | 138****1234,139****5678 |

phones 列包含多个值,违反 1NF。

✅ 修正

拆分为多行或单独建表:

复制代码
-- 方案1:多行(适合一对多)
| user_id | name | phone        |
|---------|------|--------------|
| 1       | 张三 | 138****1234  |
| 1       | 张三 | 139****5678  |

-- 方案2:单独建联系人表(推荐)
users (id, name)
user_phones (user_id, phone)

现代补充 :MySQL 5.7+ 支持 JSON 类型,但业务关键字段仍应遵守 1NF,JSON 仅用于非查询/非关联的扩展属性。


第二范式(2NF):消除部分依赖

✅ 前提
  • 表已满足 1NF
  • 存在复合主键(单主键自动满足 2NF)
✅ 核心要求
  • 所有非主键字段 必须完全依赖于整个主键,不能只依赖主键的一部分
❌ 反例(订单明细表)
复制代码
orders_detail (
    order_id,    -- 主键部分
    product_id,  -- 主键部分
    product_name, ← 仅依赖 product_id
    customer_name, ← 仅依赖 order_id
    quantity
)
PRIMARY KEY (order_id, product_id)

product_name 只与 product_id 有关,与 order_id 无关 → 部分依赖

✅ 修正:拆表
复制代码
-- 订单明细(只保留完全依赖字段)
order_items (
    order_id,
    product_id,
    quantity,
    PRIMARY KEY (order_id, product_id)
)

-- 产品表
products (
    id,
    name,
    price
)

-- 订单主表
orders (
    id,
    customer_name,
    order_date
)

🔑 口诀:2NF 解决"复合主键下的字段归属不清"问题。


第三范式(3NF):消除传递依赖

✅ 前提
  • 表已满足 2NF
✅ 核心要求
  • 所有非主键字段 不能依赖于其他非主键字段(即:非主键字段之间不能有依赖)
❌ 反例
复制代码
employees (
    id,
    name,
    department_id,
    department_name  ← 依赖 department_id,而非直接依赖 id
)

department_namedepartment_idid,形成传递依赖

✅ 修正:继续拆表
复制代码
employees (
    id,
    name,
    department_id,
    FOREIGN KEY (department_id) REFERENCES departments(id)
)

departments (
    id,
    name
)

3NF 是绝大多数业务系统的终点!它保证了:

  • 无冗余(部门名只存一次)
  • 无更新异常(改部门名只需改一行)

BCNF(Boyce-Codd 范式):3NF 的强化版

✅ 核心要求
  • 对于每一个函数依赖 X → YX 必须是超键(即 X 能唯一确定一行)
❌ 反例(特殊场景)

假设一门课只能由一位老师教,一位老师可教多门课,但一门课只有一个教室:

teacher course classroom
王老师 数学 A101
李老师 物理 B202
王老师 化学 A101

函数依赖:

  • (teacher, course) → classroom (主键)
  • course → classroom (因为每门课固定教室)
  • course → teacher (假设每门课只有一位老师)

course → classroom 中,course 不是超键 → 违反 BCNF

✅ 修正
复制代码
courses (
    course,
    teacher,
    classroom,
    PRIMARY KEY (course)  -- 因为 course 决定 teacher 和 classroom
)

💡 现实意义 :BCNF 在实际开发中较少遇到,除非有复杂业务规则。3NF 已足够


第四范式(4NF):消除多值依赖

✅ 核心要求
  • 表中不能存在非平凡的多值依赖(Multi-Valued Dependency, MVD)
❌ 反例

一个用户有多个技能,也属于多个项目:

user skill project
张三 Java 项目A
张三 Python 项目A
张三 Java 项目B
张三 Python 项目B

user →→ skilluser →→ project(多值独立)

数据爆炸:n 个技能 × m 个项目 = n×m 行!

✅ 修正:拆成两个多对多关系
复制代码
user_skills (user, skill)
user_projects (user, project)

💡 4NF 本质是处理多对多中的独立多值属性,通常通过中间表自然满足。


第五范式(5NF)/ 投影-连接范式(PJ/NF)

  • 处理连接依赖(Join Dependency)
  • 极其罕见,仅出现在高度复杂的多表关联场景
  • 普通开发者几乎不会遇到

📌 结论:了解即可,无需深究。


范式总结对比表

范式 核心目标 关键检查点 是否常用
1NF 字段原子化 无数组、重复组 ✅ 必须
2NF 消除部分依赖 复合主键下,非主键是否依赖全部主键 ✅ 常见
3NF 消除传递依赖 非主键字段是否依赖其他非主键字段 ✅✅ 最常用
BCNF 所有决定因素都是超键 函数依赖的左边是否为主键/超键 ⚠️ 少数场景
4NF 消除多值依赖 是否有多值独立导致数据爆炸 ⚠️ 特殊场景
5NF 消除连接依赖 --- ❌ 几乎不用

实际开发中:做到第几范式就够了?

强烈建议:至少满足 3NF

为什么?

  • 3NF 能解决 99% 的数据冗余和异常问题
  • 更高范式(如 4NF)往往通过合理使用中间表自然达成
  • 过度追求范式可能导致表过多、JOIN 复杂、性能下降

范式 vs 反范式:何时该"退一步"?

🔄 反范式(Denormalization)故意引入冗余以提升查询性能

适用场景:
场景 反范式做法
高频读、低频写 在订单表中冗余 customer_name
大数据量聚合 预计算并存储"每日销售额"
多表 JOIN 性能差 合并维度表到事实表(星型模型)
示例:电商订单表(3NF vs 反范式)
复制代码
-- 3NF 设计(规范)
orders (id, user_id, ...)
users (id, name, phone)

-- 反范式设计(性能优化)
orders_denorm (
    id,
    user_id,
    user_name,   -- 冗余
    user_phone,  -- 冗余
    amount
)

⚖️ 平衡原则

  • OLTP(交易系统):优先范式(保证数据一致性)
  • OLAP(分析系统):可接受反范式(追求查询速度)
  1. 设计阶段:按 3NF 建模,确保数据干净;
  2. 上线后 :根据查询性能瓶颈,局部反范式
  3. 用外键约束:在开发环境开启,保障数据完整性;
  4. 文档记录:说明哪些地方做了反范式及原因。
相关推荐
U***49832 小时前
前端性能优化插件,图片压缩与WebP转换
前端
c***V3232 小时前
前端构建工具发展,esbuild与swc性能
前端
u***u6852 小时前
前端构建工具多环境配置,开发与生产
前端
U***e632 小时前
前端构建工具迁移,Webpack到Vite
前端·webpack·node.js
煎蛋学姐2 小时前
SSM基于J2EE的山西旅游网站的设计与实现iiqmx(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·数据库·java-ee·ssm 框架·山西旅游网站·在线预订系统
百***61872 小时前
Spring的构造注入
android·java·spring
小白宗轩2 小时前
vsCode的java配置
java·vscode·python
桦说编程2 小时前
如果让我从头再来学习并发编程
java·设计模式·性能优化
Ustinian_3102 小时前
【HTML】前端工具箱实现【文本处理/JSON工具/加解密/校验和/ASCII/时间戳转换等】【附完整源代码】
前端·html·json