
🔥个人主页:Cx330🌸
❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》
《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔
《Git深度解析》:版本管理实战全解 《Qt 极境架构》MySQL 核心技术与实战
🌟心向往之行必能
🎥Cx330🌸的简介:

目录
[二、基础约束:NULL/NOT NULL 与 DEFAULT](#二、基础约束:NULL/NOT NULL 与 DEFAULT)
[1. 空属性(Null / Not Null)](#1. 空属性(Null / Not Null))
[2. 默认值(Default)](#2. 默认值(Default))
[3. 列描述(Comment)](#3. 列描述(Comment))
[4. 深入剖析 Zerofill](#4. 深入剖析 Zerofill)
[三. 核心约束:主键、自增长与唯一键](#三. 核心约束:主键、自增长与唯一键)
[1.主键(Primary Key)](#1.主键(Primary Key))
[2. 自增长(Auto_Increment)](#2. 自增长(Auto_Increment))
[3. 唯一键(Unique Key)](#3. 唯一键(Unique Key))
[主键 vs 唯一键](#主键 vs 唯一键)
[场景 A:主键的"唯一性"与"非空性"](#场景 A:主键的“唯一性”与“非空性”)
[场景 B:唯一键可以有多个(主键只能有一个)](#场景 B:唯一键可以有多个(主键只能有一个))
[场景 C:唯一键允许为 NULL(空值)](#场景 C:唯一键允许为 NULL(空值))
[💡 核心总结(博主敲黑板)](#💡 核心总结(博主敲黑板))
[四. 关联约束:外键(FOREIGN KEY)](#四. 关联约束:外键(FOREIGN KEY))
[1. 外键(Foreign Key)](#1. 外键(Foreign Key))
[五. 综合案例演练:网上商城数据库设计](#五. 综合案例演练:网上商城数据库设计)
[SQL 脚本实现](#SQL 脚本实现)
前言:
作为C++开发者,我们写代码时最执着的就是类型安全 、内存安全 以及通过
const、assert或智能指针来维护类的不变式(Class Invariant)。然而,当我们把目光移向后端存储------数据库时,光靠C++层面的校验是远远不够的。在网络波动、高并发并发写或业务逻辑漏洞面前,数据库才是守护数据的最后防线。在MySQL中,真正约束字段的是数据类型 ,但数据类型的约束过于单一(比如仅限制整型或字符型)。为了保障业务逻辑的正确性与数据的合法性,我们需要一套更强大的防御机制------表的约束。
今天,我们就来一次性吃透MySQL中的核心约束:Null/Not Null、Default、Comment、Zerofill、Primary Key、Auto_Increment、Unique Key 以及 Foreign Key!
一、什么是表的约束?
在正式深入具体约束之前,我们首先需要理解"表的约束"究竟是什么,以及为什么需要它。
对于初学者来说,常有一个误区:"既然我可以通过应用层(比如 C++、Java 或 Python 业务代码)来进行数据合法性校验,为什么还需要数据库层面的约束?"
实际上,约束的本质是一种预防机制,是从业务逻辑角度保证数据正确性、完整性和合法性的终极屏障。
-
类型约束的局限性 :MySQL 的数据类型本身就是一种最基础的约束。例如,将一个字段定义为 INT ,你就无法插入字符串。然而,类型约束过于单一。例如,一个表示"邮箱"的字段,其类型是 VARCHAR(50),但类型本身无法阻止用户插入两个一模一样的邮箱,也无法阻止插入无意义的空数据。
-
多终端与多语言协作的风险:在现代企业级架构中,一个数据库可能同时被 C++ 编写的高并发写入服务、Go 语言编写的微服务、Python 编写的数据分析脚本、甚至 DBA 的手动 SQL 共同读写。如果仅在应用层做校验,任何一方的代码漏洞、规则漏配或手动失误,都会向数据库倾倒脏数据。
-
性能与一致性的权衡:把约束规则下沉到数据库引擎层,由 MySQL 在底层原子性地、统一地进行审核,可以极大精简应用层的冗余代码,并从底层物理地掐断产生脏数据的可能。
因此,表的约束不仅是为了限制用户,更是为了保护数据库的引用完整性与实体完整性。
二、基础约束:NULL/NOT NULL 与 DEFAULT
1. 空属性(Null / Not Null)
在默认情况下,MySQL 的字段基本都允许为空(NULL)。但在实际企业级开发中,我们尽可能要保证字段不为空(NOT NULL)。
为什么不推荐使用 NULL?
-
无法参与运算 :NULL代表"无值",它与任何值进行运算都会得到 NULL。
-
难以索引与统计 :在进行聚合函数计算(如 COUNT)或建立索引时,NULL常会带来意想不到的坑。
案例:班级表设计
从业务逻辑看,班级必须有名字,也必须有上课的教室。如果不做限制,脏数据流入数据库后,业务系统就会崩溃。因此,我们必须在建表时加上not null约束:
mysql> create table myclass(
-> class_name varchar(20) not null,
-> class_room varchar(10) not null);
Query OK, 0 rows affected (0.02 sec)
如果你尝试插入一条缺少教室信息的数据,MySQL 将直接拒绝,从根本上杜绝了脏数据的产生:
mysql> insert into myclass (class_name) values('class1');
ERROR 1364 (HY000): Field 'class_room' doesn't have a default value

2. 默认值(Default)
在业务中,某些数据经常会重复出现(例如:用户的性别默认是"男",年龄默认是"0")。这时我们可以配置 default属性。
mysql> create table tt10 (
-> name varchar(20) not null,
-> age tinyint unsigned default 0,
-> sex char(2) default '男'
-> );
-
生效机制 :只有在写入数据时省略了该列的赋值,默认值才会生效。
-
注意 :NOT NULL 和 DEFAULT不需要同时对一个字段死板地设计。因为一旦设置了 DEFAULT,即便不传值,系统也会用默认值填充,所以该列不可能为空。


3. 列描述(Comment)
对 C++ 程序员来说,Comment就像是类成员变量旁的注释。它没有实际的运行约束含义,专门用来给程序员或 DBA 了解该字段的业务背景。
mysql> create table tt12 (
-> name varchar(20) not null comment '姓名',
-> age tinyint unsigned default 0 comment '年龄',
-> sex char(2) default '男' comment '性别'
-> );
-
查看方式 :使用 desc tt12;是看不到 注释信息的。必须使用 show create table tt12\G 才能查看:
mysql> show create table tt12\G
*************************** 1. row ***************************
Table: tt12
Create Table: CREATE TABLEtt12(
namevarchar(20) NOT NULL COMMENT '姓名',
agetinyint(3) unsigned DEFAULT '0' COMMENT '年龄',
sexchar(2) DEFAULT '男' COMMENT '性别'
) ENGINE=InnoDB DEFAULT CHARSET=utf8
4. 深入剖析 Zerofill
新手在看建表语句时,常对 int(10) 或 int(5) 后的括号数字感到困惑。"整型不是占4个字节吗?这里的 5 或 10 是什么意思?"
实际上,如果没有 zerofill 属性,括号内的数字 is meaningless。
Zerofill 的格式化展现
当我们对列添加 zerofill属性后,一旦存储的数值宽度小于设定的宽度(如 int(5)),MySQL 就会在左侧自动填充 0。



核心本质:它只是格式化输出!
需要强调的是,这只是显示效果,数据库内部实际存储的依然是原数值 。 我们可以用 C/C++ 程序员最熟悉的 hex() 函数(十六进制转换)来证明:
mysql> select a, hex(a) from tt3;
+-------+--------+
| a | hex(a) |
+-------+--------+
| 00001 | 1 |
+-------+--------+
十六进制输出为 1,充分证明其底层二进制存储并未发生变化。这和 C++ 中的std::setw(5) << std::setfill('0') 格式化输出完全一致!
三. 核心约束:主键、自增长与唯一键
1.主键(Primary Key)
主键是表中最核心的约束,用于唯一约束该字段里面的数据。
特点
-
不能重复,不能为空 (not null 且 unique)。
-
一张表最多只能有一个主键。
-
主键所在的列通常是整数类型。
主键的基本操作
-
创建表时指定:
create table tt13 ( id int unsigned primary key comment '学号', name varchar(20) not null ); -
追加主键 :如果建表时没有设主键,可以通过 alter补上:
alter table 表名 add primary key(字段名);

-
删除主键:
alter table 表名 drop primary key;

复合主键
在实际复杂业务中,单一字段无法保证唯一性,此时可以使用复合主键 。例如记录学生的各科成绩:学生ID + 课程代码 共同决定一条唯一的成绩。
mysql> create table tt14(
-> id int unsigned,
-> course char(10) comment '课程代码',
-> score tinyint unsigned default 60 comment '成绩',
-> primary key(id, course) -- id和course合并为复合主键
-> );
如果强行插入两个 id和 course完全相同的数据,MySQL 就会抛出主键冲突错误:ERROR 1062 (23000): Duplicate entry '1-123' for key 'PRIMARY'。

2. 自增长(Auto_Increment)
自增长通常与主键搭配使用,作为表的逻辑主键 。当不给值时,系统会自动触发自增,在当前已有最大值的基础上 +1。
特点
-
要做自增长的字段,前提是它本身必须是一个索引(Key 一栏有值)。
-
自增长字段必须是整数。
-
一张表最多只能有一个自增长。
mysql> create table tt21(
-> id int unsigned primary key auto_increment,
-> name varchar(10) not null default ''
-> );
在连续插入数据后,我们可以通过下述 SQL 语句获取上一次插入的自增 ID:
mysql> select last_insert_id();


🔥 补充:什么是索引? 索引是数据库为了快速检索数据而创建的物理存储结构,相当于图书的目录。通过目录中的页码指针,数据库不用全表扫描(O(N)),而是通过特定的查找算法(如 B+ 树,时间复杂度逼近 O(log N))快速定位数据行。
3. 唯一键(Unique Key)
很多人容易搞混主键 和唯一键。既然我们已经有了主键,为什么还需要唯一键?
唯一键的本质
一张表中往往有多个字段需要唯一性约束(例如:员工的身份证、工号、邮箱、手机号)。但是主键只能有一个。
唯一键可以完美解决多字段唯一性约束的问题。同时,唯一键允许为空(NULL),且空字段不做唯一性比较(可以有多个 NULL)。
主键 vs 唯一键
在系统设计中,我们通常建议:
-
主键(Primary Key) :设计为与当前业务无关的递增字段(如自增ID)。这样当业务规则大改时,底层主键完全不需要做任何调整,保持底层结构的稳定性。
-
唯一键(Unique Key) :用于保障业务层面的无重复约束(如身份证、工号、电子邮箱等)。
mysql> create table student (
-> id char(10) unique comment '学号,不能重复,但可以为空',
-> name varchar(10)
-> );
假设我们要设计一张 员工表(employees):
1. SQL 建表代码
CREATE TABLE employees (
emp_id INT PRIMARY KEY AUTO_INCREMENT, -- 主键:员工工号(系统内部唯一标识)
name VARCHAR(50) NOT NULL, -- 姓名
id_card CHAR(18) UNIQUE, -- 唯一键:身份证号
phone VARCHAR(11) UNIQUE -- 唯一键:手机号
);
2. 模拟插入数据,看看它们的行为差异:
场景 A:主键的"唯一性"与"非空性"
如果我们插入两个员工:
INSERT INTO employees (name, id_card, phone) VALUES ('张三', '110101199003072345', '13800138000');
-- 此时系统自动生成 emp_id = 1
INSERT INTO employees (name, id_card, phone) VALUES ('李四', '110101199205091234', '13900139000');
-- 此时系统自动生成 emp_id = 2
- 主键(
emp_id):由系统自增自动控制(1, 2, 3...),绝对不会重复,也绝对不能为空。它是数据库用来精准定位"这一行是谁"的物理钥匙。
场景 B:唯一键可以有多个(主键只能有一个)
在这张表中,我们同时约束了身份证号(id_card)和 手机号(phone)都不能重复:
-
我们不能 把 emp_id和 id_card都设为主键(因为一张表只能有一个主键)。
-
但我们可以设置多个唯一键。这样,如果有人试图用同一个手机号注册两个员工,MySQL 就会直接报错拦截。
场景 C:唯一键允许为 NULL(空值)
假设公司招了一个外国专家,他暂时还没有办好本地的身份证 ,或者某个员工入职时没有登记手机号:
-- 成功插入:身份证和手机号都留空(NULL)
INSERT INTO employees (name, id_card, phone) VALUES ('王五', NULL, NULL);
-- 再次成功插入:另一个员工也暂时没有登记手机号
INSERT INTO employees (name, id_card, phone) VALUES ('赵六', '110101199508085678', NULL);
-
唯一键的宽容度 :唯一键字段允许写入
NULL。 -
空值不冲突 :即使表里有多个员工的 phone都是 NULL,MySQL 也不会 报"重复冲突"的错误。但如果这是主键,绝对无法写入 NULL。
💡 核心总结(博主敲黑板)
| 特性 | 主键 (Primary Key) | 唯一键 (Unique Key) |
|---|---|---|
| 数量限制 | 一张表只能有一个 | 一张表可以有多个 |
| 是否允许为空 | 绝对不能 为 NULL | 允许 为 NULL |
| 设计定位 | 系统级定位:与业务无关(如自增ID),用于做表连接和索引优化。 | 业务级防重:保障真实业务数据的唯一性(如手机号、身份证、邮箱)。 |
四. 关联约束:外键(FOREIGN KEY)
1. 外键(Foreign Key)
在关系型数据库中,表与表之间绝对不是孤立存在的。外键用于定义主表(Parent Table)和从表(Child Table)之间的关联。
语法
foreign key (从表列名) references 主表名 (主表主键/唯一键)
案例设计
我们设计一个学生表(从表)和班级表(主表)的关系:
-
主表:班级表
create table myclass (
id int primary key,
name varchar(30) not null comment '班级名'
); -
从表:学生表(引入外键)
create table stu (
id int primary key,
name varchar(30) not null comment '学生名',
class_id int,
foreign key (class_id) references myclass (id) -- 关联主表
);

深度思考:如何真正理解外键?
有些同学可能会问:"如果不创建外键约束,我们只要在代码层面自己保证关联不就行了吗?"
这在理论上可行,但在高并发、多团队协作的项目中,这几乎是不可能完成的任务。 比如:学校目前只有 101 和 102 两个班。如果一个新同学入学时被录入了不存在的 103 班,而在应用层代码中漏掉了这个逻辑校验,那么脏数据就会污染数据库。
建立外键的本质,就是把这种"业务关联的审核工作"直接交下了底层的 MySQL。 提前告诉 MySQL 表之间的硬性关联,一旦用户尝试插入不符合业务逻辑的数据,MySQL 就会在最底层直接将该操作枪毙!
五. 综合案例演练:网上商城数据库设计
最后,我们用一个完整的网上商城库表设计,把上面学到的约束融会贯通:
需求
-
包含三个表:商品表(goods) 、客户表(customer) 、购买订单表(purchase)。
-
客户的姓名不能为空,邮箱必须唯一。
-
客户的性别限制为 '男' 或 '女'。
-
正确建立主外键关系。
SQL 脚本实现
-- 1. 创建数据库
create database if not exists bit32mall
default character set utf8;
use bit32mall;
-- 2. 创建商品表 (goods)
create table if not exists goods
(
goods_id int primary key auto_increment comment '商品编号',
goods_name varchar(32) not null comment '商品名称',
unitprice int not null default 0 comment '单价, 单位分', -- 避免浮点数精度丢失, 采用分作为单位
category varchar(12) comment '商品分类',
provider varchar(64) not null comment '供应商名称'
);
-- 3. 创建客户表 (customer)
create table if not exists customer
(
customer_id int primary key auto_increment comment '客户编号',
name varchar(32) not null comment '客户姓名',
address varchar(256) comment '客户地址',
email varchar(64) unique key comment '电子邮箱',
sex enum('男', '女') not null comment '性别',
card_id char(18) unique key comment '身份证'
);
-- 4. 创建购买订单表 (purchase)
create table if not exists purchase
(
order_id int primary key auto_increment comment '订单号',
customer_id int comment '客户编号',
goods_id int comment '商品编号',
nums int default 0 comment '购买数量',
-- 建立外键约束,保护引用完整性
foreign key (customer_id) references customer (customer_id),
foreign key (goods_id) references goods (goods_id)
);
结语
在 C++ 的底层世界,我们通过异常、RAII 和严谨的指针逻辑保障程序的稳健运行;而在数据库的世界,约束就是我们最强大的 RAII 守护盾牌。
一个结构设计良好、约束严密的数据库表,不仅能让后端的业务代码写起来极度清爽、优雅,更能在各种极端高并发的写入场景下保持坚不可摧的持久化完整性。
如果你觉得这篇内容对你有帮助,不妨点赞、收藏,我们下期见!