【MySQL基础】一文吃透“表的约束”:从 Null/Default 到主外键的终极安全法则

【MySQL基础】一文吃透“表的约束”:从 Null/Default 到主外键的终极安全法则 个人主页Cx330❄️个人专栏《C语言》《LeetCode刷题集》《数据结构-初阶》《C知识分享》《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔《Git深度解析》:版本管理实战全解 《Qt 极境架构》MySQL 核心技术与实战心向往之行必能Cx330的简介目录前言一、什么是表的约束二、基础约束NULL/NOT NULL 与 DEFAULT1. 空属性Null / Not Null2. 默认值Default3. 列描述Comment4. 深入剖析 Zerofill三. 核心约束主键、自增长与唯一键1.主键Primary Key2. 自增长Auto_Increment3. 唯一键Unique Key主键 vs 唯一键场景 A主键的“唯一性”与“非空性”场景 B唯一键可以有多个主键只能有一个场景 C唯一键允许为 NULL空值 核心总结博主敲黑板四. 关联约束外键FOREIGN KEY1. 外键Foreign Key深度思考如何真正理解外键五. 综合案例演练网上商城数据库设计需求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 与 DEFAULT1. 空属性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 doesnt have a default value2. 默认值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 TABLE tt12 ( name varchar(20) NOT NULL COMMENT 姓名, age tinyint(3) unsigned DEFAULT 0 COMMENT 年龄, sex char(2) DEFAULT 男 COMMENT 性别 ) ENGINEInnoDB DEFAULT CHARSETutf84. 深入剖析 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 语句获取上一次插入的自增 IDmysql 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) - );假设我们要设计一张员工表employees1. 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都是NULLMySQL 也不会报“重复冲突”的错误。但如果这是主键绝对无法写入NULL。 核心总结博主敲黑板特性主键 (Primary Key)唯一键 (Unique Key)数量限制一张表只能有一个一张表可以有多个是否允许为空绝对不能为NULL允许为NULL设计定位系统级定位与业务无关如自增ID用于做表连接和索引优化。业务级防重保障真实业务数据的唯一性如手机号、身份证、邮箱。四. 关联约束外键FOREIGN KEY1. 外键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 守护盾牌。一个结构设计良好、约束严密的数据库表不仅能让后端的业务代码写起来极度清爽、优雅更能在各种极端高并发的写入场景下保持坚不可摧的持久化完整性。如果你觉得这篇内容对你有帮助不妨点赞、收藏我们下期见