关系型数据库设计基础:约束、三大范式、表关系与表设计流程

文章目录

  • [一: 数据库约束](#一: 数据库约束)
    • [1. 什么是数据库约束](#1. 什么是数据库约束)
    • [2. 常见约束类型](#2. 常见约束类型)
      • [(1) NOT NULL(非空约束)](#(1) NOT NULL(非空约束))
      • [(2) UNIQUE(唯一约束)](#(2) UNIQUE(唯一约束))
      • [(3) DEFAULT(默认约束)](#(3) DEFAULT(默认约束))
      • [(4) PRIMARY KEY(主键约束)](#(4) PRIMARY KEY(主键约束))
      • [(5) FOREIGN KEY(外键约束)](#(5) FOREIGN KEY(外键约束))
      • [(6) CHECK(检查约束)](#(6) CHECK(检查约束))
  • [二: 表关系](#二: 表关系)
    • [1. 一对一](#1. 一对一)
    • [2. 一对多(1 : N)](#2. 一对多(1 : N))
    • [3. 多对多(M : N)](#3. 多对多(M : N))
    • [4. 关系总结](#4. 关系总结)
  • [三: 关系型数据库三大范式](#三: 关系型数据库三大范式)
  • [四: 实际表设计流程](#四: 实际表设计流程)

文章作者:当战神遇到编程

文章专栏:MySQL

欢迎大家点赞👍评论📝收藏⭐文章



一: 数据库约束

1. 什么是数据库约束

数据库约束是对表中数据进行限制的规则 ,用于保证数据的正确性完整性一致性 。简单理解:防止插入错误数据

2. 常见约束类型

约束名 作用
NOT NULL 字段不能为空
UNIQUE 字段值唯一,不能重复
PRIMARY KEY 主键,唯一且不能为空
FOREIGN KEY 外键,保证关联关系
DEFAULT 默认值
CHECK 检查数据是否满足条件

(1) NOT NULL(非空约束)

某列必须有值。

sql 复制代码
student_name VARCHAR(10) NOT NULL

表结构

(2) UNIQUE(唯一约束)

该列数据不能重复,但可以有一个或多个 NULL(不同数据库略有区别)。

sql 复制代码
student_phone VARCHAR(20) UNIQUE

表结构

(3) DEFAULT(默认约束)

插入数据时若未赋值,则使用默认值。

sql 复制代码
student_gender VARCHAR(2) DEFAULT '男'

表结构

(4) PRIMARY KEY(主键约束)

这一行/这一条记录的身份标识

sql 复制代码
student_id INT PRIMARY KEY

表结构

特点:

  • 主键的值在表中是唯一的
  • 不能为空:主键字段的值必须有内容,不能是空白或NULL
  • 只能定义一个主键;主键可以是单列,也可以是多列组合(复合主键)。
sql 复制代码
-- 复合主键例子
-- 规则: 只有当 student_name 和 class_id 同时相同时,才会被判定为重复。
CREATE TABLE student(student_name VARCHAR(10),class_id INT,PRIMARY KEY(student_name,class_id));

表结构

  • 当主键为整数类型时,通常建议开启 AUTO_INCREMENT(自增)属性,让数据库自动维护主键值,避免手动维护的麻烦。
sql 复制代码
CREATE TABLE student(student_id INT PRIMARY KEY AUTO_INCREMENT,student_name VARCHAR(10),class_id INT);

表结构

主键值交给数据库自动分配,插入时可以直接写 NULL,无需手动指定:

sql 复制代码
INSERT INTO student VALUES(NULL,'张三',1);

核心原理与特点

1.数据库内部维护了一个计数器,默认情况下,新值 = 上一次分配的值 + 1。

2.自增主键在开发中,特别常用,但对于分布式场景无法应对

(5) FOREIGN KEY(外键约束)

用于建立父表(主表)与子表(从表)之间的引用关系 ,保证数据的参照完整性,避免出现无效的 "孤儿数据"。

补充说明:父表中被关联的列必须带有索引(主键自带索引,因此外键通常引用父表的主键)。

sql 复制代码
-- 1. 切换数据库
USE java118;

-- 2. 创建父表:班级表
CREATE TABLE class(
    class_id INT PRIMARY KEY AUTO_INCREMENT,
    class_name VARCHAR(10)
);
-- 插入班级数据
INSERT INTO class VALUES(NULL,'java110'), (NULL,'java111'), (NULL,'java112');

-- 3. 创建子表:学生表(class_id 列设置外键,引用 class 表的 class_id)
CREATE TABLE student(
    student_id INT PRIMARY KEY AUTO_INCREMENT,
    student_name VARCHAR(10),
    class_id INT,
    FOREIGN KEY (class_id) REFERENCES class(class_id)
);
-- 插入学生数据(外键值必须在父表中存在)
INSERT INTO student VALUES(NULL,'张三',1), (NULL,'李四',2), (NULL,'王五',3);

外键的核心约束规则

  • 子表约束(插入 / 修改限制):

    如果父表中不存在对应的外键值,子表无法插入或修改该外键列。

    例如:不能给学生插入一个不存在的班级 ID(如 class_id=99)。

  • 父表约束(删除 / 修改限制):

    如果父表中的数据正被子表引用,则该条数据无法直接删除或修改。

    例如:不能直接删除还有学生关联的班级,否则会导致学生的 class_id 变成无效数据。

补充说明:查看外键约束

查看子表结构时,Key 列若显示为 MUL,就代表该列设置了外键约束:

外键实战:电商项目中的「逻辑删除」方案

在电商等实际项目中,外键的 "父表删除限制" 会带来一个典型矛盾,我们通常用逻辑删除来解决。

1.业务矛盾:外键约束 vs 商品下架

  • 业务场景:商家想把 "衬衫" 下架(相当于删除商品),但订单表中已经有大量该商品的历史订单。
  • 矛盾点:外键约束会禁止直接删除商品,否则订单表中的商品 ID 会变成无效数据,导致历史订单混乱。

2. 解决方案:逻辑删除

核心原理

不真正物理删除数据,而是通过一个 标记位(Flag) 来表示该数据的 "有效 / 失效" 状态,让数据 "假死" 而非消失。

实现步骤

(1) 给父表增加标记列

在商品表中新增一个状态字段,例如 isOk:

○ 1 表示商品有效(上架状态)

○ 0 表示商品失效(下架状态)

(2) 下架操作:改为更新标记位

不再执行 DELETE 删除语句,而是用 UPDATE 把标记位改成 0:

sql 复制代码
-- 把商品id=1的商品标记为下架
UPDATE 商品表 SET isOk = 0 WHERE 商品id = 1;

(3) 用户查询:过滤失效数据

查询商品列表时,只返回标记为有效的数据,用户感知不到 "已下架商品" 的存在:

sql 复制代码
-- 只给用户展示未下架的商品
SELECT * FROM 商品表 WHERE isOk = 1;

3.逻辑删除的核心优势

  • 保留历史数据:已下架商品的历史订单记录依然完整可查,不会出现数据断裂。
  • 数据安全可恢复:误操作下架后,只需把 isOk 改回 1 即可恢复商品,无需复杂的数据恢复流程。
  • 兼容外键约束:无需删除数据,自然避开了外键对父表删除的限制,同时依然保证了数据的参照完整性。

(6) CHECK(检查约束)

限制字段取值范围。

例如年龄不能小于6。

sql 复制代码
CREATE TABLE student(age INT CHECK(age > 6),name VARCHAR(20));

二: 表关系

表与表之间常见有四种关系类型:

一对一(1 : 1)

一对多(1 : N)

多对多(M : N)

没关系

1. 一对一

含义:

一张表中的一条记录,只对应另一张表中的一条记录。

示例:

用户表 与 用户详情表

user(用户表)

id username
1 张三

user_info(详情表)

user_id 身份证号 地址
1 xxx 北京

说明:

  • 一个用户只有一份详情
  • 一份详情只属于一个用户

一对一 (1:1) 实现代码

核心要点 : 在从表的外键上添加 UNIQUE 约束,确保一个主表记录只能被引用一次。

sql 复制代码
-- 1. 创建用户主表
CREATE TABLE user (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(20)
);

-- 2. 创建用户详情表
CREATE TABLE user_info (
    -- user_id 既是外键,也必须是唯一的,从而实现 1:1
    user_id INT UNIQUE, 
    id_card VARCHAR(18),
    address VARCHAR(100),
    -- 建立外键关联
    FOREIGN KEY (user_id) REFERENCES user(id)
);

2. 一对多(1 : N)

含义

一张表中的一条记录,对应另一张表中的多条记录。

示例

学院表 与 学生表

college(学院表)

id 学院名
1 计算机学院

student(学生表)

id 姓名 college_id
1001 张三 1
1002 李四 1

说明:

  • 一个学院有多个学生
  • 一个学生只属于一个学院

一对多 (1:N) 实现代码

核心要点: 外键直接建立在"多"的一方(学生表)即可,不需要加 UNIQUE。

sql 复制代码
-- 1. 创建学院表 (一)
CREATE TABLE college (
    id INT PRIMARY KEY AUTO_INCREMENT,
    college_name VARCHAR(20)
);

-- 2. 创建学生表 (多)
CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT,
    student_name VARCHAR(10),
    college_id INT, -- 外键列
    -- 多个学生可以关联同一个学院ID
    FOREIGN KEY (college_id) REFERENCES college(id)
);

3. 多对多(M : N)

含义:

两张表中的多条记录彼此对应多条记录。

示例:

学生表 与 课程表

  • 一个学生可选多门课
  • 一门课可被多个学生选

student

id 姓名

course

id 课程名

student_course(中间表)

student_id course_id
1001 C01
1001 C02
1002 C01

多对多 (M:N) 实现代码

核心要点: 必须创建一个独立的中间表,用来存放两个表的主键关联。

sql 复制代码
-- 1. 创建学生表
CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(10)
);

-- 2. 创建课程表
CREATE TABLE course (
    id VARCHAR(10) PRIMARY KEY,
    course_name VARCHAR(20)
);

-- 3. 创建中间表 (选课表)
CREATE TABLE student_course (
    student_id INT,
    course_id VARCHAR(10),
    -- 建议:将两个外键设为联合主键,防止同一个学生重复选同一门课
    PRIMARY KEY (student_id, course_id),
    -- 分别建立外键关联
    FOREIGN KEY (student_id) REFERENCES student(id),
    FOREIGN KEY (course_id) REFERENCES course(id)
);

4. 关系总结

关系类型 举例 实现方式
一对一 用户-详情 外键 + 唯一约束
一对多 学院-学生 多方加外键
多对多 学生-课程 建中间表

三: 关系型数据库三大范式

第一范式(1NF)列不可再分

核心要求:字段必须具有原子性(不可再分)

每一列 只能存一个值,不能存多个值。

不满足第一范式的例子

学号 姓名 班级名 学校信息
101 张三 Java一班 清华大学, 北京市海淀区, 010-8888

满足第一范式的例子(合格的表)

学号 姓名 班级名 学校名称 学校地址 学校电话
101 张三 Java一班 清华大学 北京市海淀区 010-8888

注意:

  • 数据库的"入场券": 第一范式是关系型数据库的最基本要求。如果不满足 1NF,则不能被称为真正的关系型数据库。
  • 原子性的标准: 原子性不是绝对的,而是根据业务需求 决定的。
    ○ 例如: 如果业务只需要显示全名,那么 姓名 就是原子的;但如果业务需要区分"姓"和"名",那么 姓名 就得拆分为 姓 和 名 两列。

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

核心定义:

在满足第一范式(1NF)的基础上,消除非主键字段对候选键的"部分函数依赖"。

1.必须要懂的底层概念 (术语拆解)

  • 候选键 (Candidate Key) vs 主键 (Primary Key)

    候选键 : 表中能唯一标识一行数据的列(或列组合)。

    主键 : 从候选键中选出来的"正职"班长。

    联合主键 : 由多个列共同组成的唯一主键。此时,单独的每一列只能叫"候选键",合在一起才叫"主键"。

  • 函数依赖

    完全函数依赖 : 必须通过整个主键 才能确定某个数据。(2NF 要求 )

    部分函数依赖 : 只通过主键中的一部分列 ,就能确定某个数据。 ( 2NF 禁止)

2.案例剖析

  • 这张表中使用学号 + 课程名定义联合主键来唯一标识一个学生某门课程的成绩
  • 这张表中学生姓名 依赖于学号 , 依赖于课程名 ,学分 依赖于课程名 , 依赖于学号,这两个非关键字段都不满足第二范式的核心定义

改正:拆成三个表

学生表

Id 学号 学生姓名 年龄 性别
1 10001 张三 18
2 10002 李四 19
3 10003 王五 18

课程表

Id 课程名 学分
1 MySQL 50
2 Java 60
3 C++ 60

成绩表

学生Id 课程Id 成绩
1 1 98
2 1 100
3 1 89
1 2 100
2 2 99
3 3 98

只有单列主键的表通常天然满足第二范式。

如果表没有满足第二范式,可能出现的问题有:

1.数据冗余 :学生的姓名,年龄学分都重复的出现,造成大量的数据冗余

  1. 更新异常 :假设需要调整MySQL的学分,就需要修改所有记录中关于MySQL的记录.如果部分记录更新失败,就会出现同一课程学分不一致的情况,表现为数据不一致

  2. 插入异常 :⽬前这样的设计,成绩与每⼀⻔课和学⽣都有对应关系,也就是说只有学⽣参加选修课程考试取得了成绩才能⽣成⼀条记录。当有⼀⻔新课还没有学⽣参加考试取得成绩之前,那么这⻔新课在数据库中是不存在的,因为成绩为空时记录没有意义。

  3. 删除异常:把毕业学⽣的考试数据全都删除,此时课程和学分的信息也会被删除掉,有可能导致⼀段时间内,数据库⾥没有某⻔课程和学分的信息。

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

定义:在满⾜第⼆范式的基础上,消除非主键字段对非主键字段的依赖(消除传递依赖)。

一、先理解什么叫传递依赖

如果存在:

sql 复制代码
主键 -> A -> B

说明:

  • A 依赖主键
  • B 又依赖 A

那么 B 就是间接依赖主键,这叫传递依赖。

举个例子:

学号 姓名 年龄 所属学院 学院地址 学院电话

存在传递依赖:学号-> 所在学院-> 学院电话 ,存在传递依赖的表不满足第三范式。

改正:只需要拆分成两张表。

1.学院表

学院编号 学院名 学院电话 学院地址

2.学生表

学号 姓名 年龄 学院编号

这样设计后,两张表中的非主键字段都直接依赖各自主键;学生表通过外键(学院编号)与学院表建立关联关系。

四: 实际表设计流程

1. 需求分析(明确存什么)

  • 理清业务: 搞清楚系统有哪些实体(如用户、商品、订单)以及它们之间的关系。
  • 确定字段: 列出每个实体需要记录的信息(如用户有姓名、手机号、注册时间)。

2. 概念设计(画 E-R 图)

  • 识别关系: 确定实体间是一对一(1:1)、一对多(1:N)还是多对多(N:N)。
  • 绘图: 使用 E-R 图(实体-联系图)可视化表达,防止漏掉关键关联。

3.逻辑设计(定表结构)

  • 表转换: 将实体转为表,字段转为列。
  • 数据类型: 为每个字段选择合适的类型(如数字用 INT,短文本用 VARCHAR,时间用 DATETIME)。
  • 主键设计: 每张表必须有一个唯一标识(通常是自增 ID 或 UUID)。

4. 范式约束(减少冗余)

  • 遵循三范式(3NF):
    1NF: 字段不可再分(原子性)。
    2NF: 非主键字段必须完全依赖主键(消除部分依赖)。
    3NF: 非主键字段不能相互依赖(消除传递依赖,如:表里存了部门ID,就不应再存部门名称)。
  • 注:有时为了性能会适当"反范式"(冗余少量字段减少连表)。

5. 物理优化(提升性能)

  • 索引设计: 给高频查询的字段(如 username、order_sn)建立索引。

  • 约束设置: 设置 NOT NULL(非空)、DEFAULT(默认值)、UNIQUE(唯一)。

  • 存储引擎: 选择合适的引擎(如 MySQL 默认的 InnoDB)。

6. 评审与落地(产出 SQL)

  • 设计评审: 与开发团队核对是否满足业务需求。
  • 编写 DDL: 编写 CREATE TABLE 语句并执行。
  • 维护文档: 更新数据库字典,方便后期维护。
相关推荐
其实防守也摸鱼1 小时前
《SQL注入进阶实验:基于sqli-Labs的报错注入(Error-Based Injection)实战解析》
网络·数据库·sql·安全·网络安全·sql注入·报错注入
Ting.~1 小时前
软件设计师备考笔记【day3】-数据库
数据库·笔记
Bert.Cai1 小时前
MySQL FLOOR()函数详解
数据库·mysql
小碗羊肉1 小时前
【MySQL | 第七篇】索引
数据库·mysql
m0_495496411 小时前
SQL中如何获取前N个最大值并排除自己_利用窗口函数限制
jvm·数据库·python
m0_740653221 小时前
mysql如何提取日期中的年份_使用year函数从日期中截取
jvm·数据库·python
hanyi_qwe1 小时前
Mysql 与 Nginx 双机高可用
数据库·mysql·nginx
S1998_1997111609•X2 小时前
login:/-system.web,dex.dmp,b-scode:app·%
网络·数据库·百度·facebook·twitter
运气好好的2 小时前
mysql数据库日志文件过大如何清理_定期备份与重置日志文件
jvm·数据库·python