MySQL(五):数据约束与表设计

目录

一、为什么需要约束

[1. 什么是约束(Constraint)](#1. 什么是约束(Constraint))

[2. 约束的作用](#2. 约束的作用)

二、空属性(NULL)

[1. NULL 与 NOT NULL](#1. NULL 与 NOT NULL)

[2. 案例](#2. 案例)

[3. NULL 查询注意事项](#3. NULL 查询注意事项)

三、默认值(DEFAULT)

[1. 什么是默认值](#1. 什么是默认值)

[2. 语法与案例](#2. 语法与案例)

[3. 默认值插入与分析](#3. 默认值插入与分析)

[4. DEFAULT 与 NOT NULL](#4. DEFAULT 与 NOT NULL)

四、列描述(COMMENT)

[1. 什么是列描述](#1. 什么是列描述)

[2. COMMENT 语法](#2. COMMENT 语法)

[3. 查看列描述](#3. 查看列描述)

[4. 案例:为用户表添加字段说明](#4. 案例:为用户表添加字段说明)

五、ZEROFILL

[1. 什么是 ZEROFILL](#1. 什么是 ZEROFILL)

[2. ZEROFILL 效果演示](#2. ZEROFILL 效果演示)

[3. UNSIGNED 与 ZEROFILL](#3. UNSIGNED 与 ZEROFILL)

[六、主键(PRIMARY KEY)](#六、主键(PRIMARY KEY))

[1. 主键的概念与核心作用](#1. 主键的概念与核心作用)

[2. 主键约束语法](#2. 主键约束语法)

[3. 主键管理](#3. 主键管理)

[4. 案例:学生表主键设计](#4. 案例:学生表主键设计)

七、自增长(AUTO_INCREMENT)

[1. 自增长的概念与工作机制](#1. 自增长的概念与工作机制)

[2. 语法规则与起始值修改](#2. 语法规则与起始值修改)

[3. 案例:用户 ID 自动生成](#3. 案例:用户 ID 自动生成)

八、唯一键(UNIQUE)

[1. 唯一键的概念与语法规则](#1. 唯一键的概念与语法规则)

[2. 主键与唯一键的区别](#2. 主键与唯一键的区别)

[3. 案例:手机号唯一约束](#3. 案例:手机号唯一约束)

九、索引初识

[1. 为什么需要索引](#1. 为什么需要索引)

[2. 索引与约束的关系](#2. 索引与约束的关系)

[十、外键(FOREIGN KEY)](#十、外键(FOREIGN KEY))

[1. 为什么需要外键](#1. 为什么需要外键)

[2. 外键约束的概念与语法](#2. 外键约束的概念与语法)

[3. 外键约束的联动行为](#3. 外键约束的联动行为)

[4. 案例:班级表与学生表](#4. 案例:班级表与学生表)

[5. 外键使用建议](#5. 外键使用建议)

十一、综合案例

[1. 商店购物表设计](#1. 商店购物表设计)

[2. 约束在实际项目中的应用](#2. 约束在实际项目中的应用)

总结


一、为什么需要约束

在关系型数据库管理系统中,数据库不仅承担着存储数据 的功能,更核心的任务是保证数据的正确性与有效性。为了实现这一目标,MySQL 引入了 "约束" 这一核心机制


1. 什么是约束(Constraint)

约束是关系型数据库系统强制执行的一种数据验证规则。它通常在创建表或修改表结构时,定义在特定的列(字段)或列组合上

从底层实现来看,约束是数据库管理系统在执行数据变更操作(如 INSERT、UPDATE、DELETE)时自动触发的校验机制。任何不符合约束规则的数据操作请求,都会被数据库拒绝执行并抛出错误。通过这种方式,约束将业务逻辑中的数据规范直接沉淀到了数据库层面


2. 约束的作用

我们可以对比存在约束与缺乏约束下的数据状态的差异。约束的核心作用,正是为了解决各类数据质量问题

保证数据的完整性

数据的完整性是指数据的精确性和可靠性。没有约束的限制,数据库将退化为普通的文件存储,导致以下三种完整性遭到破坏:

  • 实体完整性: 要求表中的每一行记录都是唯一的、可标识的

    • 缺乏约束的后果: 允许存在相同记录,导致系统无法精准定位或更新特定的业务实体
  • 域完整性: 要求单元格中的数据必须符合特定数据类型、格式和取值范围

    • 缺乏约束的后果: 用户的 "年龄" 字段可能会被填入负数或字符,"电子邮箱" 字段可能会被填入任意文本,导致数据失去可用性
  • 参照完整性: 维护多张表之间的关联关系,确保外链数据的存在性

    • 缺乏约束的后果: 学生表中的 "班级ID" 可能指向一个不存在的班级;或者一个班级被删除后,该班级下的学生记录依然残留,产生 "孤儿数据"

降低业务复杂度

数据校验可以在两个层面进行:应用层数据库层

  • 缺乏数据库约束: 开发者必须在每次执行操作前,编写大量的条件判断代码(如检查是否为空、是否重复等)。一旦某段代码出现漏洞,或者有其他外部系统直接操作数据库,脏数据就会立即渗入

  • 启用数据库约束: 数据库作为数据流向的最后一道防线,提供了全局一致的校验标准。这不仅简化了应用层的代码量,更避免了并发操作或代码漏洞导致的数据不一致问题

提升一致性与健壮性

在高并发场景下,缺乏约束通常会导致不可逆的灾难:

  • 并发冲突问题: 两个用户同时注册同一个手机号。如果没有唯一性约束,两条相同的手机号记录会同时写入数据库,导致后续登录逻辑混乱

  • 级联操作: 在删除一个父表记录时,如果缺乏约束联动,关联的子表记录将变成无法被检索和维护的悬空数据

二、空属性(NULL)

在 MySQL 中,控制字段是否允许为空是表设计时必须确定的首要要素


1. NULL 与 NOT NULL

在数据库设计中,NULL 和 NOT NULL 用于声明某个字段是否允许留空

什么是 NULL

NULL 在数据库中表示缺失的值未知的状态不适用的数据 。需要特别强调的是,NULL 既不等于数字 0,也不等于空字符串

概念 含义 存储空间 参与数值运算结果
NULL 数据未知或不存在 占用特殊标志位(NULL 列表) 结果一律为 NULL
空字符串 长度为 0 的字符串数据 占用 0 或 1 字节 按字符串逻辑处理
数字 0 数值零 占用固定字节 按正常数值计算

NULL 的特点

  • 存储机制: 在 InnoDB 存储引擎中,行格式行头部包含一个 "NULL 值列表"。如果某列允许为 NULL 且实际值为 NULL,它不会在实际的数据列存储区占用空间,而是通过行头部的位图进行标记

  • 三值逻辑: 传统逻辑只有 "真(TRUE)" 和 "假(FALSE)"。而在引入 NULL 后,MySQL 的逻辑运算演变为三值逻辑,即包含 TRUE、FALSE 和 UNKNOWN。任何与 NULL 进行的标准比较操作(如 =, <>),其结果都是 UNKNOWN


2. 案例

在业务设计中,对于核心业务验证字段,应当强制声明为 NOT NULL(非空约束)

语法规则

非空约束直接在列定义的数据类型之后声明:

sql 复制代码
CREATE TABLE table_name (
    column_name data_type NOT NULL
);

案例:学生姓名不能为空

在设计学生基础信息表时,"姓名" 是识别学生实体的关键信息,不允许出现未知或缺失的情况

sql 复制代码
CREATE TABLE student (
    id INT,
    -- 声明姓名不可为空
    name VARCHAR(50) NOT NULL, 
    email VARCHAR(100)
);

验证

  1. 正常插入数据:

    sql 复制代码
    INSERT INTO student (student_id, student_name, email) VALUES (1, '张三', 'zhangsan@example.com');
  2. 触发非空约束限制: 若尝试隐式或显式地向 name 插入 NULL 值:

    sql 复制代码
    INSERT INTO student (id, name, email) VALUES (2, NULL, 'lisi@example.com');

3. NULL 查询注意事项

在互联网企业或高并发系统的开发规范(如《阿里 Java 开发手册》)中,通常建议将所有字段均设置为 NOT NULL,并给定默认值。其原因在于:

  1. **性能优化:**允许为 NULL 的列会使索引统计和空间计算更加复杂

  2. **避免异常:**允许 NULL 意味着应用层在读取数据后,必须频繁进行 if (value == null) 的判空处理,增加了系统崩坏的隐患

  3. 消除查询陷阱: 使用子查询时,如果子查询结果集中包含 NULL 值,整个查询将返回空结果集,这种隐蔽的逻辑漏洞在生产环境中极难排查

三、默认值(DEFAULT)

在明确了空属性的概念后,另一个常见需求是:当用户在插入数据时未明确指定某一列的值,系统应当能够自动填充一个预设的初始值。这就是默认值约束的作用


1. 什么是默认值

默认值是绑定在表字段上的一个属性。当向表中插入新记录时,如果没有提及该字段,或者显式指定使用默认值,MySQL 会自动将预先设定好的初始值填入该字段中

默认值常用于具有高频重复初值的业务字段。例如:用户的账号状态通常默认为 "正常",订单的支付状态默认为 "待支付",或者记录的创建时间默认为系统当前时间。通过设置默认值,可以大幅度简化写入数据时的参数传递


2. 语法与案例

默认值约束通常在建表时声明,直接紧跟在数据类型或非空约束之后

语法规则

sql 复制代码
CREATE TABLE table_name (
    column_name data_type DEFAULT default_value
);

default_value 可以是常量(如数字、字符串),也可以是特定的系统函数(如获取当前时间的 CURRENT_TIMESTAMP)

案例:用户注册表设计

在设计用户注册表时,业务要求用户的 "状态码(status)" 在注册时默认为 1(代表正常),且 "注册时间" 自动生成为当前的系统时间

sql 复制代码
CREATE TABLE user_account (
    id INT,
    name VARCHAR(50) NOT NULL,
    -- 状态码默认值为 1
    status INT DEFAULT 1,
    -- 注册时间默认值为系统当前时间
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

3. 默认值插入与分析

为了更直观地理解默认值的生效机制,以下通过三种不同的插入场景对数据表的行为进行验证。

场景一:隐式应用默认值

在 INSERT 语句中,不列出拥有默认值的字段

sql 复制代码
INSERT INTO user_account (id, name) VALUES (1, '用户A');

系统会自动为 status 填入 1,为 created_at 填入当前执行该语句的时间戳

场景二:显式应用默认值

在 INSERT 语句中列出了字段名,但值的部分直接使用 DEFAULT 关键字

sql 复制代码
INSERT INTO user_account (id, name, status) VALUES (2, '用户B', DEFAULT);

数据状态: status 字段捕获到 DEFAULT 关键字,自动填充其预设值 1

场景三:覆盖默认值

在 INSERT 语句中为字段提供了具体的、不同于默认值的数据

sql 复制代码
INSERT INTO user_account (id, name, status) VALUES (3, '用户C', 0);

4. DEFAULT 与 NOT NULL

在实际开发中,DEFAULT 约束经常与 NOT NULL 约束组合使用,例如:status INT NOT NULL DEFAULT 1。它们在协同工作时遵循以下逻辑:

  1. 如果用户插入数据时没有提供 该字段的值,DEFAULT 机制首先介入,自动填充默认值(如 1)。由于填入了非空数据,此时不会触发 NOT NULL 报错,数据成功写入

  2. 如果用户在插入数据时,显式地为该字段传入了 NULL 值,此时 DEFAULT 机制不会生效(因为用户已经主动提供了值,尽管这个值是 NULL)。随后 NOT NULL 约束进行检查,发现该字段被赋值为 NULL,从而拒绝写入并抛出错误

四、列描述(COMMENT)

在协同开发或长期维护的场景中,保持结构的可读性与可维护性至关重要。MySQL 提供了列描述定义机制,允许开发者将字段的业务含义写在数据库的元数据中


1. 什么是列描述

列描述是 MySQL 在数据中存储的字典信息,用于为特定的数据列(或数据表)添加解释性文本

与前文提到的 NOT NULL 或 DEFAULT 不同,COMMENT 不具备任何实质性的数据约束能力 ,它不会拦截或修改任何写入的数据。它的核心价值在于文档化。通过将字段的业务逻辑、取值含义直接写入表结构,可以有效避免因人员变更或文档缺失导致的代码维护困难


2. COMMENT 语法

列描述在定义列时声明,其关键字为 COMMENT,后紧跟包含在单引号内的字符串描述文本

语法规则

sql 复制代码
CREATE TABLE table_name (
    column_name data_type [约束条件] COMMENT '描述文本'
);

注意事项

  • 描述文本的最大长度限制为 1024 个字符

  • 当使用 ALTER TABLE 修改字段类型或宽度时,必须重新指定 COMMENT 属性,否则原有的列描述将会丢失


3. 查看列描述

由于列描述属于数据库的元数据,常规的 DESCRIBE table_name 命令无法查看到列描述信息。要获取包含 COMMENT 在内的完整列定义,通常采用以下两种方法

使用 SHOW FULL COLUMNS

该命令能够以表格形式完整输出列的所有扩展属性

sql 复制代码
SHOW FULL COLUMNS FROM table_name;

使用 SHOW CREATE TABLE

该命令可以反向导出构建该数据表时的完整 SQL 语句

sql 复制代码
SHOW CREATE TABLE table_name;

4. 案例:为用户表添加字段说明

以下构建一张用户个人资料表(user_profile),并通过 COMMENT 明确标明各个字段的业务边界与枚举值含义

sql 复制代码
CREATE TABLE user_profile (
    user_id INT PRIMARY KEY COMMENT '系统唯一用户ID',
    nickname VARCHAR(50) NOT NULL COMMENT '用户昵称,不可为空',
    gender TINYINT DEFAULT 0 COMMENT '性别代号:0-未定义,1-男性,2-女性',
    account_balance DECIMAL(10, 2) DEFAULT 0.00 COMMENT '账户可用余额,单位为元'
);

查看验证

执行查验命令:

sql 复制代码
SHOW FULL COLUMNS FROM user_profile;

五、ZEROFILL

在定义整数类型字段时,经常会注意到如 INT(11) 这样的写法,其中的数字通常代表显示宽度。而 ZEROFILL(零填充) 则是与显示宽度紧密相关的一种填充约束属性


1. 什么是 ZEROFILL

ZEROFILL (零填充) 是 MySQL 特有的一种显式格式化约束。当某个整数列被指定为 ZEROFILL时,如果该列实际存储的数值位数小于定义时指定的显示宽度,MySQL 会自动在数值的左侧填充零(0),直到补齐到指定的宽度为止

需要明确的是,ZEROFILL 不会改变数据的存储大小,也不会改变数值的实际大小。它只在数据被检索出来、呈现给客户端时改变展现形式


2. ZEROFILL 效果演示

以下通过构建一张测试表,演示不同整数宽度在启用 ZEROFILL 后的实际输出形态

创建测试表

创建一个包含标准整数列与零填充整数列的表,显示宽度均设置为 4:

sql 复制代码
CREATE TABLE padding_test (
    id INT,
    normal_val INT(4),
    zero_filled_val INT(4) ZEROFILL
);

插入并输出数据

向表中写入两行测试数据,分别包含小于 4 位和大于 4 位的整数:

sql 复制代码
INSERT INTO padding_test (id, normal_val, zero_filled_val) VALUES (1, 25, 25);
INSERT INTO padding_test (id, normal_val, zero_filled_val) VALUES (2, 12345, 12345);

SELECT * FROM padding_test;
  • 对于单号 1: 实际数值为 25(长度为 2)。zero_filled_val 列由于指定了宽度为 4,系统自动在左侧补齐两个 0,显示为 0025。而 normal_val 保持原样输出

  • 对于单号 2: 实际数值为 12345(长度为 5),已经超过了定义的显示宽度 4。此时 ZEROFILL 不再进行截断或填充,而是完整输出原数值 12345


3. UNSIGNED 与 ZEROFILL

在 MySQL 中,ZEROFILL 与 UNSIGNED(无符号)属性存在强绑定关系

一旦为某个整数列声明了 ZEROFILL,MySQL 会自动将该列转为 UNSIGNED 类型。即使在建表语句中显式写了 SIGNED(有符号),该设定也会被强制覆盖

底层逻辑: > 零填充的目的是为了格式化对齐形式。如果允许字段存储负数,负号将会与填充的 0 产生显示冲突(例如无法规范定义 -0025 或 00-25 的展现形式)。因此,系统通过强制转化为无符号数来消减这一不确定性

ZEROFILL 的局限性

尽管 ZEROFILL 在特定场景下能够提供便利,但在现代数据库架构与生产实践中,应当谨慎使用或避免使用,原因如下:

  1. 非标准 SQL 规范: ZEROFILL 是 MySQL 的独有扩展,不属于 ANSI SQL 标准。这意味着一旦未来发生数据库迁移(如迁移至 PostgreSQL 或 Oracle),该特性将无法兼容

  2. 表现层职责错配: 现代软件开发倡导 "数据库负责数据存储,应用层负责数据展示"。诸如补零、货币格式化等表现层需求,应当由后端或前端框架来处理,而不应由数据库引擎承担

  3. 官方已明文弃用: 从 MySQL 8.0.17 版本开始,ZEROFILL 属性已被官方标记为弃用,并在未来的主要版本中将会被移除。同时,整数类型的显示宽度(如 INT(M) 中的 M)也一并被废弃。因此,在新系统的表设计中,不应再依赖此机制

六、主键(PRIMARY KEY)

在关系型数据库的设计中,主键是最为核心的约束类型。无论是从保证数据完整性的逻辑层面,还是从优化存储性能的物理层面,主键都发挥着不可替代的作用


1. 主键的概念与核心作用

主键是表中用于唯一标识每一条记录的一个字段或一组字段

主键的硬性物理约束

只要一个字段被声明为主键,它将自动具备以下两个底层约束属性:

  • NOT NULL(非空): 主键列绝对不允许存储 NULL 值

  • UNIQUE (唯一): 主键列的值在整张表中必须是唯一的,不允许出现重复值

为什么必须设计主键

主键的作用主要体现在以下两个维度:

  • 维护完整性: 主键确保了表中的每一行数据都对应一个现实世界中的独立实体。没有主键,数据库将无法精准定位、修改或删除特定的某一行记录

  • 决定物理存储顺序: MySQL 默认的 InnoDB 存储引擎是 "索引组织表"。InnoDB 必须依赖主键来构建底层的数据结构------聚簇索引 。表中的实际数据行在磁盘上的物理存储顺序,就是按照主键的先后顺序进行排列的。如果一张表在创建时没有显式定义主键,InnoDB 会自动选择一个非空的唯一索引作为主键 ;若连唯一的非空索引也没有,系统会在后台自动生成一个名为RowID 的 6 字节隐藏列作为主键。因此,显式定义主键可以避免系统资源的不必要消耗


2. 主键约束语法

主键的定义方式根据业务场景的不同,主要分为列级定义表级定义 以及复合主键定义

1. 单列主键的创建

列级定义(最常用的标准方式)

在定义列的同时,直接在数据类型后声明 PRIMARY KEY

sql 复制代码
CREATE TABLE employee (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(50) NOT NULL
);

表级定义

在所有列声明完毕后,在表的末尾独立声明主键。这种方式常用于需要清晰组织表结构的场景

sql 复制代码
CREATE TABLE department (
    dept_id INT,
    dept_name VARCHAR(50) NOT NULL,
    PRIMARY KEY (dept_id)
);

2. 复合主键

在某些特殊的业务场景中,单一列无法唯一标识一行记录,需要将两个或多个列组合起来共同作为主键。复合主键只能使用表级定义语法

sql 复制代码
CREATE TABLE course_selection (
    student_id INT,
    course_id INT,
    selected_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    -- 声明由学生ID和课程ID共同构成主键
    PRIMARY KEY (student_id, course_id)
);

对于复合主键,只有当组合中的所有列的值都完全相同时,才会被判定为重复。例如,允许插入 (1, 101) 和 (1, 102),但再次插入 (1, 101) 则会触发主键冲突。此外,复合主键中的任意一列都不能为 NULL


3. 主键管理

在实际的生产环境中,管理员或开发者需要对主键进行查验、追加或移除操作

查看主键

通过执行 DESCRIBE 或 SHOW CREATE TABLE 命令,可以直观地确认主键的状态。在 DESCRIBE 结果中,主键列的 Key 字段会被标记为 PRI

动态查看主键

如果在创建表时遗漏了主键,且该表当前没有重复或为空的数据,可以通过 ALTER TABLE 语句进行追加

sql 复制代码
-- 将 student 表的 id 字段设置为主键
ALTER TABLE student ADD PRIMARY KEY (id);

由于一张表只能有一个主键约束,因此删除时不需要指定列名。另外,如果主键列同时具备自增长属性,则必须先移除自增长属性,才能顺利执行删除主键的操作


4. 案例:学生表主键设计

以下通过一个标准的学生基本信息表,展示单列主键在实际表设计中的应用

编写创表语句

sql 复制代码
CREATE TABLE school_student (
    student_no INT COMMENT '学号,唯一标识',
    student_name VARCHAR(50) NOT NULL COMMENT '姓名',
    id_card VARCHAR(18) NOT NULL COMMENT '身份证号',
    PRIMARY KEY (student_no)
);

约束行为验证

  1. 正常插入数据:

    sql 复制代码
    INSERT INTO school_student (student_no, student_name, id_card) 
    VALUES (20260101, '张华', '11010120000101111X');
  2. 触发主键重复限制: 尝试插入一条具有相同学号的记录

    sql 复制代码
    INSERT INTO school_student (student_no, student_name, id_card) 
    VALUES (20260101, '李明', '11010120000202222X');

    系统报错:

  3. 触发主键非空限制: 尝试显式将学号设为 NULL 写入

    sql 复制代码
    INSERT INTO school_student (student_no, student_name, id_card) 
    VALUES (NULL, '王强', '11010120000303333X');

    系统报错:

七、自增长(AUTO_INCREMENT)

在设计主键时,确保其唯一性与连续性是核心要求。如果在应用层通过代码计算并维护一个递增的主键,在高并发的场景下极易导致主键冲突。为了解决这一痛点,MySQL 提供了自增长属性


1. 自增长的概念与工作机制

自增长是专属于整数类型字段的一种特殊属性。它的核心功能是:当向表中插入新记录时,如果开发者没有显式为该列提供数值,MySQL 会自动生成一个唯一的、递增的整数并填入该列

自增长的限制

  1. 数据类型限制: 自增长属性只能应用于整数类型字段

  2. 索引依赖限制: 被声明为自增长的列必须被定义为键(Key)。在绝大多数生产实践中,自增长列同时也是表的主键(PRIMARY KEY),但它也可以被定义为唯一键(UNIQUE)

  3. 数量限制: 一张数据表有且只能有一个自增长字段

底层工作机制

在 InnoDB 存储引擎中,每张含有自增长列的表都在内存中维护着一个自增长计数器

  • 当执行 INSERT 操作时,如果自增长列被赋予了 NULL、0 或者在语句中被完全省略,MySQL 会读取该计数器的当前值分配给该列,随后将计数器执行 +1 操作

  • 如果显式插入了一个大于当前计数器值的数据(例如当前计数器为 5,用户强行插入了 10),系统会接受该设定,并直接将内存中的计数器值更新为 10 + 1 = 11。后续的自动生成将从 11 开始


2. 语法规则与起始值修改

自增长属性通常与主键约束配合使用,直接声明在列定义的最后

建表语法

sql 复制代码
CREATE TABLE table_name (
    column_name INT AUTO_INCREMENT PRIMARY KEY
);

修改自增长起始值

默认情况下,自增长的初始值从 1 开始。但在某些业务场景下(例如隐藏真实业务量),需要自定义初始值。可以通过 ALTER TABLE 语句进行调整:

sql 复制代码
-- 将指定表的自增长计数器初始值修改为 1000
ALTER TABLE table_name AUTO_INCREMENT = 1000;

修改的初始值必须大于当前表中已存在的自增长列的最大值,否则该修改将不会生效


3. 案例:用户 ID 自动生成

以下通过构建一张用户基础注册表(app_user),演示自增长属性在实际数据写入时的各种行为特征

创建数据表

在建表的同时,指定自增长初始值为 100:

sql 复制代码
CREATE TABLE app_user (
    user_id INT AUTO_INCREMENT PRIMARY KEY COMMENT '自动生成的唯一用户ID',
    username VARCHAR(50) NOT NULL COMMENT '用户名'
) AUTO_INCREMENT = 100;

验证不同插入场景的行为

为了测试自增长机制,我们执行以下几种不同的写入操作:

sql 复制代码
-- 场景一:隐式触发(在语句中完全省略自增长列)
INSERT INTO app_user (username) VALUES ('用户A');

-- 场景二:显式传入 NULL 触发
INSERT INTO app_user (user_id, username) VALUES (NULL, '用户B');

-- 场景三:显式传入 0 触发
INSERT INTO app_user (user_id, username) VALUES (0, '用户C');

-- 场景四:显式覆盖(人为指定一个较大的特定数值)
INSERT INTO app_user (user_id, username) VALUES (200, '用户D');

-- 场景五:覆盖后的后续自动生成测试
INSERT INTO app_user (username) VALUES ('用户E');

执行数据全表扫描查看结果

八、唯一键(UNIQUE)

在设计数据表时,经常会遇到一些字段,它们虽然不是表的主键,但要求不能重复。例如:用户的手机号、电子邮箱、身份证号等。为了满足这种 "全局唯一但非主键" 的业务需求,MySQL 提供了唯一键约束


1. 唯一键的概念与语法规则

唯一键约束用于确保特定列中的所有数据都是唯一的。它可以在列级或表级进行声明

与主键在一张表中只能有一个的严格限制不同,一张数据表可以定义多个唯一键约束

语法示例

列级定义

直接在列声明的数据类型后追加 UNIQUE 关键字

sql 复制代码
CREATE TABLE member_account (
    member_id INT PRIMARY KEY,
    -- 声明电子邮箱为唯一键
    email VARCHAR(100) UNIQUE,
    -- 声明手机号为唯一键
    phone VARCHAR(20) UNIQUE
);

表级定义

在所有列定义完成后,在表末尾独立声明,这种方式同样支持定义多列组合的复合唯一键

sql 复制代码
CREATE TABLE organization_node (
    node_id INT PRIMARY KEY,
    org_code VARCHAR(30),
    node_name VARCHAR(50),
    -- 表级声明唯一键
    UNIQUE KEY uk_org_code (org_code)
);

2. 主键与唯一键的区别

尽管主键和唯一键都能起到防止数据重复的作用,但它们在底层设计和业务定位上存在本质区别。为了便于理解,现将两者的核心差异对比归纳如下:

主键 唯一键
数量限制 一张表有且只能有一个 一张表可以有多个
是否允许为空 绝对不允许为 NULL 允许为 NULL
空值的重复处理 不涉及 允许存在多个 NULL 值(分述如下)
存储影响 决定数据的物理组织顺(聚簇索引) 构建辅助索引以加速查询(二级索引)
核心业务定位 用于唯一标识和区分整行记录实体 用于防止特定业务字段产生重复数据

在 MySQL 的 InnoDB 存储引擎中,NULL 被视为 "未知的状态"。因此,唯一键约束允许表中存在多条该列值为 NULL 的记录。系统不会认为多个 NULL 是重复的数据,这一行为完全符合 SQL 标准规范


3. 案例:手机号唯一约束

以下通过构建一张通讯录表(user_contact),来观察唯一键约束在实际数据写入时的行为特征

创建数据表

sql 复制代码
CREATE TABLE user_contact (
    contact_id INT PRIMARY KEY AUTO_INCREMENT,
    contact_name VARCHAR(50) NOT NULL,
    -- 手机号声明为唯一键
    phone_number VARCHAR(20) UNIQUE
);

验证不同写入场景的行为

我们向该表中依次尝试写入五条记录,观察数据库的响应结果:

sql 复制代码
-- 场景一:正常顺序写入不同的手机号
INSERT INTO user_contact (contact_name, phone_number) VALUES ('用户A', '13800000001');
INSERT INTO user_contact (contact_name, phone_number) VALUES ('用户B', '13800000002');

-- 场景二:尝试插入已存在的重复手机号
INSERT INTO user_contact (contact_name, phone_number) VALUES ('用户C', '13800000001');

-- 场景三:显式插入第一个 NULL 值
INSERT INTO user_contact (contact_name, phone_number) VALUES ('用户D', NULL);

-- 场景四:显式插入第二个 NULL 值
INSERT INTO user_contact (contact_name, phone_number) VALUES ('用户E', NULL);

执行数据全表扫描,最终表内成功留存的数据状态如下:

由于用户 C 的插入操作触发约束冲突而被中断,虽然写入失败,但在底层依然消耗了自增长计数器,导致后续成功写入的用户 D 的 contact_id 跳跃至 4

九、索引初识

在前面讨论主键与唯一键约束时,我们多次提到了 "索引" 这一概念。约束与索引是数据库中相辅相成的两个重要机制。本章将对索引进行初步的引入,阐述其存在的核心价值以及它与数据约束之间的关系


1. 为什么需要索引

随着系统业务的运行,数据库表中的数据量会呈线性或指数级增长。当表内包含数十万甚至数千万条记录时,如果我们要寻找某一条特定的数据,数据库引擎只能从第一行开始,逐行读取并比对每一条记录,直到遍历完整个表。这种检索方式在数据库中被称为全表扫描

全表扫描会引发大量的磁盘 I/O 操作,及其消耗 CPU 资源。在高并发的生产环境中,全表扫描会导致查询响应时间剧烈拉长,进而引发系统性能雪崩。为了消除这种检索瓶颈,数据库引入了索引

什么是索引

索引是数据库管理系统中的一种专门用于加速数据检索的特殊物理存储结构

如果把整张数据表比作一本厚重的技术书籍,那么数据行就是书中的具体页面内容,而索引就是书前那一页页精简的目录

  • 物理本质: 索引本身也是一种数据,需要占用一定的磁盘存储空间。它通常由表中的一列或多列组合而成,并由数据库引擎在后台维护一套特定的高效数据结构(在 InnoDB 存储引擎中,最核心的数据结构是 B+树

  • 工作机制: 当执行查询时,数据库引擎会首先在索引结构中进行树状层级的快速匹配,精准获取到目标数据所在的磁盘物理地址,然后直接定位并读取对应的行记录。通过这种机制,查询操作能够跳过绝大多数无关的数据块,将原本需要扫描几百万行数据的操作,缩减为仅需几次磁盘 I/O 即可完成


2. 索引与约束的关系

在关系型数据库的底层实现中,约束与索引是深度绑定、共生共存的

职责层面的区别

两者在逻辑概念上的定位截然不同:

  • 约束 属于业务逻辑与数据质量规范的范畴。它的核心职责是 "校验",即确保写入的数据符合业务边界,防止脏数据渗入

  • 索引 属于物理存储与查询性能优化的范畴。它的核心职责是 "加速",即确保读取数据的过程尽可能高效

底层实现层面的强绑定

尽管职责不同,但在底层实现上,InnoDB 直接利用索引来作为实施特定约束的硬件支撑

  1. 主键约束与聚簇索引: 当你为表声明一个 PRIMARY KEY 时,MySQL 会在底层自动为该列创建一个聚簇索引。主键所具备的唯一性、非空性,正是通过这个聚簇索引的节点排他性来强制保证的

  2. 唯一约束与唯一索引: 当你为某些列声明 UNIQUE 唯一键约束时,MySQL 会在后台自动为这些列创建一个同名的唯一索引

    • 为什么必须这么做? 试想,如果用户尝试插入一个新的手机号,系统必须校验该手机号是否在表中已存在。如果底层没有索引,系统为了执行这次 "唯一性校验",就必须去全表扫描所有数据。而通过构建唯一索引,系统便可以在微秒级别内判定该值是否重复

唯一索引是唯一约束的底层技术实现,唯一约束是唯一索引在业务层面的逻辑表象

在绝大多数情况下,当你在 MySQL 中创建了主键或唯一约束,你就已经隐含地创建了对应的索引;反之,若删除了这些索引,对应的约束限制也会同步失效

十、外键(FOREIGN KEY)

在前面讨论的所有约束中,其作用范围都局限在单一的数据表内部。但在现实世界的业务模型中,实体与实体之间往往存在着复杂的关联关系(例如:学生属于某个特定的班级,订单包含某些特定的商品)。为了在数据库层面维护这种跨表数据的关联性与一致性,MySQL 引入了外键约束


1. 为什么需要外键

在关系型数据库中,为了消除数据冗余并提高存储效率,我们通常会将数据拆分到不同的专门表中,并通过某个相同的字段将它们连接起来

然而,如果没有约束限制,多表之间的数据一致性将难以为继。例如,存在一张 "班级表" 和一张 "学生表" ,学生表中有一个字段存储 "班级 ID"。若缺乏约束,可能会发生以下数据混乱:

  1. 录入错误: 向学生表插入一条新记录,其班级ID填入了 999 ,但班级表中根本没有 ID 为 999 的班级

  2. 删除引发悬空: 班级表里某个班级因为业务调整被删除了,但学生表里属于该班级的几十名学生记录依然残留,他们持有着一个已经不存在的班级ID,变成 "孤儿数据"

外键约束正是为了解决上述跨表不一致问题而诞生的,它是关系型数据库实现参照完整性的核心技术手段


2. 外键约束的概念与语法

外键约束的本质是让一张表(子表)的某个字段,去显式引用另一张表(父表)中的主键或唯一键

父表与子表

  • 父表(主键表 / 被引用的表): 提供数据源头、被其他表引用的表。外键所指向的父表字段,必须具备主键或唯一键约束

  • 子表(外键表 / 引用表): 创建了外键约束、去引用父表数据的表。子表外键列的数据类型,必须与父表被引用列的数据类型完全一致

语法规则

外键通常在子表的定义中声明,语法如下:

sql 复制代码
CREATE TABLE child_table (
    column_name data_type,
    -- 定义外键约束
    [CONSTRAINT constraint_name] FOREIGN KEY (child_column) 
        REFERENCES parent_table (parent_column)
        [ON DELETE action_option]
        [ON UPDATE action_option]
);

3. 外键约束的联动行为

当父表中的数据发生变更(删除或修改主键)时,这些变更会直接影响到子表中已经引用了该数据的行。MySQL 允许通过 action_option 规定四种不同的联动处理策略:

  1. RESTRICT (限制 / 默认行为): 如果子表中存在关联记录,父表对应的记录绝对不允许被删除或修改。执行该操作时系统会直接报错拦截

  2. CASCADE (级联): 父表记录被删除或修改时,子表中所有关联的记录同步自动被删除或修改。这种策略适用于强隶属关系的表(如订单表与订单明细表)

  3. SET NULL(设为空): 父表记录被删除或修改时,子表中关联记录的外键字段会自动被设置为 NULL。(注:使用此策略时,子表的外键列不能有 NOT NULL 约束)

  4. NO ACTION: 在 MySQL 中,其作用与 RESTRICT 完全相同


4. 案例:班级表与学生表

以下通过构建班级表(school_class)作为父表,学生表(school_student_fk)作为子表,来演示外键约束的控制力

创建父表与子表

sql 复制代码
-- 1. 创建父表:班级表
CREATE TABLE school_class (
    class_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '班级唯一ID',
    class_name VARCHAR(50) NOT NULL COMMENT '班级名称'
);

-- 2. 创建子表:学生表(采用级联删除、限制更新策略)
CREATE TABLE school_student_fk (
    student_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '学生唯一ID',
    student_name VARCHAR(50) NOT NULL COMMENT '学生姓名',
    fk_class_id INT COMMENT '所属班级ID',
    -- 声明外键约束,指向父表的主键
    CONSTRAINT fk_student_class FOREIGN KEY (fk_class_id) 
        REFERENCES school_class (class_id)
        ON DELETE CASCADE 
        ON UPDATE RESTRICT
);

验证实验

1. 向父表与子表正常写入合规数据

sql 复制代码
-- 先往父表插入两个班级:1班 和 2班
INSERT INTO school_class (class_id, class_name) VALUES (1, '一班'), (2, '二班');

-- 往子表插入学生,分别关联到 1班 和 2班
INSERT INTO school_student_fk (student_name, fk_class_id) VALUES ('小明', 1);
INSERT INTO school_student_fk (student_name, fk_class_id) VALUES ('小红', 2);

2. 触发子表插入拦截(不存在的父表引用)

尝试为新学生指定一个根本不存在的班级 ID 99:

sql 复制代码
INSERT INTO school_student_fk (student_name, fk_class_id) VALUES ('孤立同学', 99);

系统报错:

bash 复制代码
INSERT INTO school_student_fk (student_name, fk_class_id) VALUES ('孤立同学', 99);
ERROR 1452 (23000): Cannot add or update a child row: 
a foreign key constraint fails (`test_db`.`school_student_fk`, 
CONSTRAINT `fk_student_class` FOREIGN KEY (`fk_class_id`) REFERENCES 
`school_class` (`class_id`) ON DELETE CASCADE ON UPDATE RESTRICT)

3. 触发父表更新拦截

由于在定义外键时,更新策略指定为了 RESTRICT,我们尝试修改已被引用的 1 班的 ID:

sql 复制代码
UPDATE school_class SET class_id = 10 WHERE class_id = 1;

系统报错:

bash 复制代码
mysql> UPDATE school_class SET class_id = 10 WHERE class_id = 1;
ERROR 1451 (23000): Cannot delete or update a parent row: 
a foreign key constraint fails (`test_db`.`school_student_fk`, 
CONSTRAINT `fk_student_class` FOREIGN KEY (`fk_class_id`) REFERENCES 
`school_class` (`class_id`) ON DELETE CASCADE ON UPDATE RESTRICT)

4. 验证父表级联删除

我们直接从父表中删除 2 班(该班级下有学生小红):

sql 复制代码
DELETE FROM school_class WHERE class_id = 2;

此时分别查看两张表的数据状态:


5. 外键使用建议

尽管外键在维护数据一致性方面表现完美,但在现代高并发、大规模的互联网系统设计中,通常不建议在数据库层面强行启用外键约束 。诸如《阿里Java开发手册》等业界规范也明确指出:不得使用外键与级联,一切外键概念必须在应用层解决

其核心理由在于:

  1. 性能瓶颈: 每次在子表执行插入或修改时,数据库都必须同步去父表进行一次高昂的检索校验。在高并发的写入场景下,这会带来显著的性能损耗,并极易引发死锁

  2. 不利于分库分表: 当数据量极大、系统需要进行分库分表和分布式架构改造时,跨物理节点的强外键约束在物理上将无法实施

  3. 维护成本高: 强外键会导致数据迁移和测试数据构造变得及其繁琐,必须严格按照先后顺序才能删除或插入数据

折中实践:

在实际开发中,我们通常保留外键的逻辑概念,但将一致性校验的逻辑交由后端服务去控制。数据库层面的外键约束,则更多用于传统单机、对性能要求不高但对一致性要求极严的系统(如财务系统)中

十一、综合案例

在掌握了 MySQL 的各类数据约束后,接下来将通过一个具体的电商购物场景,演示如何在实际的项目表设计中综合运用这些约束


1. 商店购物表设计

根据业务需求,我们需要为一个商店的客户及购物情况设计数据库表。该系统由三张表组成:商品表客户表购买订单表

业务需求规约

  1. 主外键设计: 每张表需具备唯一标识的主键;订单表需通过外键分别关联客户表与商品表

  2. 客户姓名约束: 客户姓名必须存在,不能为空值

  3. 邮箱约束: 每个客户的邮箱在全表范围内必须保持唯一,不能重复

  4. 性别约束: 限制客户的性别只能为 "男" 或 "女"

  5. 常识性约束: 商品单价、购买数量不能为负数;订单生成时间应自动记录

创表 SQL 语句实现

sql 复制代码
-- 1. 创建商品表 (goods)
CREATE TABLE goods (
    goods_id INT AUTO_INCREMENT PRIMARY KEY COMMENT '商品编号,自增长主键',
    goods_name VARCHAR(100) NOT NULL COMMENT '商品名称,不可为空',
    unitprice DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '单价,不可为空',
    category VARCHAR(50) COMMENT '商品类别',
    provider VARCHAR(100) COMMENT '供应商名称'
) COMMENT '商品信息表';

-- 2. 创建客户表 (customer)
CREATE TABLE customer (
    customer_id INT AUTO_INCREMENT PRIMARY KEY COMMENT '客户号,自增长主键',
    name VARCHAR(50) NOT NULL COMMENT '姓名,要求不能为空值',
    address VARCHAR(255) COMMENT '住址',
    email VARCHAR(100) UNIQUE COMMENT '邮箱,要求不能重复',
    -- 使用 ENUM 枚举类型限制性别的取值范围范围
    sex ENUM('男', '女') NOT NULL DEFAULT '男' COMMENT '性别,限制为男或女',
    card_id VARCHAR(18) UNIQUE COMMENT '身份证号,业务层通常也要求唯一'
) COMMENT '客户信息表';

-- 3. 创建购买订单表 (purchase)
CREATE TABLE purchase (
    order_id INT AUTO_INCREMENT PRIMARY KEY COMMENT '购买订单号,自增长主键',
    customer_id INT NOT NULL COMMENT '客户号,关联客户表',
    goods_id INT NOT NULL COMMENT '商品号,关联商品表',
    nums INT NOT NULL DEFAULT 1 COMMENT '购买数量,默认为1',
    purchase_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '购买时间',
    
    -- 设置外键约束,维护参照完整性
    CONSTRAINT fk_purchase_customer FOREIGN KEY (customer_id) 
        REFERENCES customer (customer_id) 
        ON DELETE RESTRICT 
        ON UPDATE CASCADE,
        
    CONSTRAINT fk_purchase_goods FOREIGN KEY (goods_id) 
        REFERENCES goods (goods_id) 
        ON DELETE RESTRICT 
        ON UPDATE CASCADE
) COMMENT '购买订单明细表';

设计要点解析

  • 性别限制: 案例中采用了 ENUM('男', '女') 数据类型。MySQL 的 ENUM 类型在底层会将其转化为整数存储,同时在写入时会自动校验传入的字符串。如果尝试向 sex 字段插入其他值,数据库将直接报错,实现了业务对性别的合法性校验

  • **金额与数量的类型选择:**商品单价采用了 DECIMAL(10, 2) 类型,而非 FLOAT 或 DOUBLE。这是因为 DECIMAL 是定点数类型,能精确计算钱款,防止浮点数在频繁计算时产生精度丢失

    • 购买数量 nums 给定了默认值 1,符合标准的购物篮行为

2. 约束在实际项目中的应用

在实际的企业级项目开发中,如何使用约束往往需要在安全性性能灵活性之间进行权衡。以下总结了实际生产环境中的几条应用原则:

物理约束与逻辑约束

在数据库层面强制执行的约束(如用 FOREIGN KEY 声明外键、用 CHECK 限制范围)被称为物理约束 ;而在应用层通过业务逻辑进行校验的约束被称为逻辑约束

  • 核心原则: 主键、非空与唯一应当坚决使用物理约束。 这些约束能够直接对磁盘上的索引结构产生正向优化,且校验代价极低

  • 外键与复杂条件校验: 考虑到系统在高并发下的扩展性以及分库分表的需求,现代互联网项目通常将外键与复杂的数值范围校验降级为逻辑约束。不在数据库中建立强外键关联

防御性编程

在标准的前后端分离架构中,数据校验通常会发生三次:

  1. 第一次: 前端页面(如表单提交前校验手机号格式、是否为空)

  2. 第二次: 后端服务(如使用校验框架进行入参判空)

  3. 第三次: 数据库约束(如字段本身的 NOT NULL、UNIQUE)

前端的校验是为了提升用户体验,后端的校验是为了阻断恶意攻击和非法请求,而数据库的约束则是保护企业核心资产的最后一道防线 。不能因为后端写了校验代码,就在建表时随意放宽约束。一旦后端代码产生逻辑漏洞,缺乏约束的数据库将会迅速被脏数据填满,清洗历史脏数据的成本是极其高昂的

多变性对约束设计的影响

在设计约束时,必须具备一定的前瞻性,避免 "过度设计"

例如,某些电商系统在初期将用户的手机号设置为了 NOT NULL UNIQUE。但随着业务发展,系统引入了第三方快捷登录,允许用户先注册、后绑定手机号。此时原本的物理非空约束就会直接卡死新的业务流程

因此,在遇到这类可能随业务发生变更的属性时,更建议将其设计为允许为空或移至逻辑层校验,以保持系统架构的灵活性

总结

综上所述,我们学习了最常用的几类约束,包括非空(NOT NULL)、默认值(DEFAULT)、主键(PRIMARY KEY)、自增长(AUTO_INCREMENT)、唯一键(UNIQUE)以及外键(FOREIGN KEY)等内容,并通过多个案例理解了它们在实际开发中的作用

可以发现,数据类型决定了数据如何存储,而约束则决定了数据是否允许以某种形式存储。合理地使用约束,不仅能够保证数据的完整性和一致性,还能够在数据库层面避免大量非法数据的产生

至此,我们已经具备了设计一张规范数据表所需的基础知识。不过,一张设计良好的数据表如果无法高效地完成数据的增删查改,也无法发挥数据库真正的价值

因此,在下一篇中,我们将正式进入 MySQL 数据操作部分,学习 INSERT、UPDATE、DELETE 以及 SELECT 等常用 SQL 语句,真正开始对数据进行管理与查询