05.MySQL表的约束
MySQL表的约束
MySQL表的约束
1. 空属性
2. 默认值
3. 列描述
4. zerofill
5. 主键
6. 自增长
7. 唯一键
8. 外键
9. 综合案例
MySQL表的约束
说到 MySQL 表的约束,这绝对是数据库设计里绕不开的一个话题。很多人一提“约束”,第一反应可能是字段的数据类型,比如 INT 不能存字符串,VARCHAR 有长度限制。这当然是最基本的一层限制,但光靠这些,远远不够。就像你家门口装了密码锁,结果厨房窗户忘关了——防得了一部分,还是会漏。
这时候就得靠表级的各种约束来兜底,从业务逻辑的角度出发,帮你把不合理的数据挡在门外。
举个栗子🌰:假设你建了一个用户表,里面有个“年龄”字段,类型是 INT。这时候要是有人输入个 -100 或者 999999999 岁,数据库是能存下没错,但你一看就知道这不靠谱。这种数值范围的控制,其实属于“检查约束(CHECK)”。不过早期的 MySQL 并不支持这玩意儿(8.0 开始才支持),所以我们今天先不展开这个,重点聊聊其他你在开发里经常用到的约束类型。
这篇文章我们就来盘讲讲这些实用技能:空属性、默认值、列描述、zerofill、主键、自增长、唯一键、外键。最后还会用一个综合案例,把这些知识点串起来,帮你真正掌握这些“看起来简单但常常出问题”的细节。
1. 空属性
1.1 NULL和NOT NULL的相爱相杀
数据库里默认所有字段都是允许为空的(NULL),就像你家的冰箱——什么都能塞进去。但实际开发中,咱们得学会"断舍离",把不该空的字段锁死。为啥?因为空值是个麻烦精!它参与运算时会直接让结果变NULL,就像往火锅里倒可乐——全毁了。
举个例子🌰:假设你统计销售额,某个订单金额是NULL,那么SELECT SUM(price)
的结果也会是NULL。这时候老板要是问"今天赚了多少",你只能尴尬地回一句:“不知道啊,有笔订单金额没填…”
所以遇到必填项,一定要用NOT NULL。比如班级表的教室字段:
CREATE TABLE class(class_name VARCHAR(20) NOT NULL,classroom VARCHAR(20) NOT NULL
);
这时候插入数据时,这两个字段就必须填,否则MySQL会直接报错:
INSERT INTO class(class_name) VALUES('三年二班');
-- 报错:Field 'classroom' doesn't have a default value
1.2 NULL的隐藏陷阱
很多人以为NULL就是"没有值",其实它更像"未知值"。比如两个NULL比较时,既不等于也不等于不,永远返回UNKNOWN。这会导致查询时出现意想不到的结果:
SELECT * FROM users WHERE email = NULL; -- 查不到任何数据
SELECT * FROM users WHERE email IS NULL; -- 才能查到空邮箱用户
1.3 NOT NULL的进阶玩法
有时候我们会给字段设置默认值,这时候再加NOT NULL就显得多余了。比如:
CREATE TABLE user(id INT PRIMARY KEY AUTO_INCREMENT,gender ENUM('男','女') DEFAULT '男' NOT NULL
);
这里的NOT NULL其实可以省略,因为默认值已经保证了字段不为空。但要注意默认值的类型匹配,比如用字符串’0’当默认值,字段类型却是INT的话,可能会触发隐式转换。
2. 默认值
2.1 DEFAULT的魔法时刻
默认值就像自动售货机——当你不投币时,它自己吐出预设的商品。比如用户表的性别字段:
CREATE TABLE user(id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(20),gender ENUM('男','女') DEFAULT '男'
);
这时候插入数据时,如果不指定gender字段,就会自动填充"男":
INSERT INTO user(name) VALUES('张三');
-- 实际插入的是('张三', '男')
2.2 默认值的进阶套路
默认值不仅能用常量,还能用表达式(MySQL 8.0+支持):
CREATE TABLE orders(order_id INT PRIMARY KEY AUTO_INCREMENT,create_time DATETIME DEFAULT NOW(),expire_time DATETIME DEFAULT (NOW() + INTERVAL 7 DAY)
);
不过要注意,同一个字段不能同时有默认值和NOT NULL约束(除非默认值明确指定),否则会触发冲突。
2.3 默认值的隐藏彩蛋
对于日期时间类型字段,默认值有特殊规则:
DATETIME
默认值只能是常量,不能用函数TIMESTAMP
会自动设置当前时间作为默认值(如果未显式指定)
比如:
CREATE TABLE test(id INT PRIMARY KEY AUTO_INCREMENT,dt DATETIME, -- 默认值为NULLts TIMESTAMP -- 默认值自动变为CURRENT_TIMESTAMP
);
3. 列描述
3.1 COMMENT的文艺复兴
列描述就像给数据库字段写小纸条,方便后来人看懂你的设计思路。比如用户表:
CREATE TABLE user(id INT PRIMARY KEY COMMENT '用户ID',name VARCHAR(20) COMMENT '用户真实姓名',nickname VARCHAR(20) COMMENT '用户昵称(可为空)'
);
通过SHOW CREATE TABLE user;
就能看到这些注释:
CREATE TABLE `user` (`id` int(11) NOT NULL COMMENT '用户ID',`name` varchar(20) NOT NULL COMMENT '用户真实姓名',`nickname` varchar(20) DEFAULT NULL COMMENT '用户昵称(可为空)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 描述的最佳实践
- 业务含义:说明字段的业务逻辑,比如
status TINYINT COMMENT '状态:1-待支付 2-已支付 3-已发货'
- 数据来源:标明数据是怎么来的,比如
score DECIMAL(5,2) COMMENT '根据考试成绩自动同步'
- 变更记录:重要字段的变更历史,比如
address VARCHAR(255) COMMENT '2022.3新增,原地址字段已废弃'
3.3 描述的隐藏功能
结合INFORMATION_SCHEMA.COLUMNS
表,可以实现自动化文档生成:
SELECT COLUMN_NAME, COLUMN_COMMENT
FROM information_schema.columns
WHERE TABLE_SCHEMA='your_db' AND TABLE_NAME='user';
4. zerofill
4.1 数字显示的艺术
zerofill就像给数字穿上西装打上领带——让它看起来更正式。比如订单编号:
CREATE TABLE orders(order_id INT(6) ZEROFILL PRIMARY KEY AUTO_INCREMENT
);
插入数据时:
INSERT INTO orders() VALUES();
-- 实际显示order_id为000001
注意这里的INT(6)不是指6位数字,而是显示宽度。底层存储还是标准的INT类型(4字节)。
4.2 zerofill的连带效应
使用zerofill会自动触发UNSIGNED属性:
CREATE TABLE test(a INT(5) ZEROFILL);
-- 实际相当于 INT(5) UNSIGNED ZEROFILL
所以字段只能存储正数,负数插入会变成0。
4.3 使用场景分析
适合需要固定显示位数的业务场景:
- 学号:202301010001(年份+学院代码+序号)
- 发票号码:0000123456
- 产品编号:P000001
但要注意,这种格式化更适合前端处理,数据库层面保持纯粹数字更利于计算。
5. 主键
5.1 数据的身份证
主键就像每个人的身份证号码——必须唯一且不能为空。创建学生表:
CREATE TABLE student(stu_id CHAR(10) PRIMARY KEY COMMENT '学号(唯一标识)',name VARCHAR(20)
);
这时候插入重复学号会直接报错:
INSERT INTO student VALUES('2023010101','张三');
INSERT INTO student VALUES('2023010101','李四');
-- 报错:Duplicate entry '2023010101' for key 'PRIMARY'
5.2 主键的进阶操作
-
删除主键:
ALTER TABLE student DROP PRIMARY KEY;
注意:如果该列有自增属性,需要先删除自增
-
修改主键:
ALTER TABLE student MODIFY stu_id CHAR(12); -- 修改字段类型不影响主键约束
-
复合主键:
CREATE TABLE cart(user_id INT,product_id INT,quantity INT,PRIMARY KEY(user_id, product_id) );
这时候单个字段可以重复,但组合必须唯一
5.3 主键选择的玄学
-
自增主键 vs 业务主键
自增主键(如AUTO_INCREMENT)更利于索引性能,业务主键(如身份证号)更直观,需要根据场景权衡 -
UUID的另类玩法
CREATE TABLE orders(order_id CHAR(36) PRIMARY KEY DEFAULT UUID(),... );
适合分布式系统,但会占用更多存储空间
6. 自增长
6.1 AUTO_INCREMENT的魔法
自增字段就像自动步枪——每次扣动扳机都会自动装弹。创建文章表:
CREATE TABLE articles(article_id INT AUTO_INCREMENT PRIMARY KEY,title VARCHAR(100)
);
插入数据时:
INSERT INTO articles(title) VALUES('MySQL约束详解');
-- article_id自动分配为1
INSERT INTO articles(title) VALUES('性能优化技巧');
-- article_id自动分配为2
6.2 自增的隐藏技巧
-
指定初始值:
CREATE TABLE users(id INT AUTO_INCREMENT PRIMARY KEY ) AUTO_INCREMENT = 1000;
-
跳增现象:
如果插入失败或事务回滚,自增值不会回退。比如插入100后失败,下一个值会是101而不是100 -
手动赋值:
INSERT INTO articles(article_id, title) VALUES(10000, '特别推荐'); -- 下次自增值从10001开始
6.3 自增的性能考量
-
并发问题:
InnoDB引擎使用互斥锁来确保自增的连续性,在高并发场景可能成为瓶颈 -
缓存配置:
innodb_autoinc_lock_mode
参数影响自增行为,需要根据业务调整 -
迁移风险:
导出导入数据时,记得检查自增字段的当前值:SHOW TABLE STATUS LIKE 'articles'; -- 查看Auto_increment列
7. 唯一键
7.1 除主键外的唯一保障
唯一键就像班级里的学号和电话号码——都可以唯一标识学生,但只能选一个当主键。创建用户表:
CREATE TABLE user(id INT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(20) UNIQUE,email VARCHAR(50) UNIQUE
);
这时候用户名和邮箱都必须唯一:
INSERT INTO user(username, email)
VALUES('zhangsan', 'zhangsan@example.com');-- 插入相同用户名会失败
INSERT INTO user(username, email)
VALUES('zhangsan', 'zs@example.com');
-- 报错:Duplicate entry 'zhangsan' for key 'username'
7.2 唯一键的骚操作
-
复合唯一键:
CREATE TABLE exam(student_id INT,subject VARCHAR(20),score INT,UNIQUE(student_id, subject) );
保证同一个学生同一科目只有一条记录
-
空值处理:
唯一键允许有多个NULL值(这与主键不同):INSERT INTO user(username) VALUES(NULL); INSERT INTO user(username) VALUES(NULL); -- 两条记录都能成功插入
-
延迟约束:
在事务中,唯一约束检查可以延迟到提交时:SET CONSTRAINTS ALL DEFERRED; -- 需要配合支持的存储引擎
7.3 唯一键的优化技巧
-
前缀索引:
对长字符串字段,可以只索引前缀:CREATE TABLE products(id INT PRIMARY KEY,product_code VARCHAR(100) UNIQUE ); -- 改为 CREATE UNIQUE INDEX idx_code ON products(product_code(20));
-
合并索引:
如果某个查询经常同时用到username和email,可以创建联合唯一索引:CREATE UNIQUE INDEX idx_user_email ON user(username, email);
8. 外键
8.1 表之间的羁绊
外键就像亲子关系——孩子必须知道自己爹是谁。创建订单表:
CREATE TABLE orders(order_id INT PRIMARY KEY AUTO_INCREMENT,user_id INT,FOREIGN KEY(user_id) REFERENCES users(id)
);
这时候插入订单时:
-- 假设users表中没有id=100的用户
INSERT INTO orders(user_id) VALUES(100);
-- 报错:Cannot add or update a child row
8.2 外键的连环反应
-
级联更新:
当父表主键变更时,子表自动更新:CREATE TABLE orders(order_id INT PRIMARY KEY AUTO_INCREMENT,user_id INT,FOREIGN KEY(user_id) REFERENCES users(id) ON UPDATE CASCADE );
-
级联删除:
删除父表记录时,自动删除子表关联数据:FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
-
置空操作:
删除父表记录时,将子表外键字段设为NULL:FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL -- 注意:字段必须允许NULL
8.3 外键的性能博弈
-
锁机制:
修改父表主键时,会锁定子表相关记录,可能引发死锁 -
批量导入:
导入大量数据时,建议先关闭外键检查:SET FOREIGN_KEY_CHECKS=0; -- 执行导入操作 SET FOREIGN_KEY_CHECKS=1;
-
索引优化:
外键字段必须有索引,否则会影响性能:CREATE INDEX idx_user ON orders(user_id); -- 如果创建外键时未自动创建
9. 综合案例
9.1 商城系统的数据设计
让我们来设计一个简单的电商系统,包含三个核心表:
9.1.1 商品表(goods)
CREATE TABLE goods(goods_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID',goods_name VARCHAR(100) NOT NULL COMMENT '商品名称',unitprice DECIMAL(10,2) NOT NULL COMMENT '单价',category VARCHAR(50) COMMENT '分类',provider VARCHAR(100) COMMENT '供应商',stock INT DEFAULT 0 COMMENT '库存',INDEX idx_category(category)
) ENGINE=InnoDB;
9.1.2 客户表(customer)
CREATE TABLE customer(customer_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '客户ID',name VARCHAR(20) NOT NULL COMMENT '姓名',address VARCHAR(200) COMMENT '住址',email VARCHAR(50) UNIQUE COMMENT '邮箱',sex ENUM('男','女') COMMENT '性别',card_id CHAR(18) UNIQUE COMMENT '身份证',reg_time DATETIME DEFAULT NOW() COMMENT '注册时间'
) ENGINE=InnoDB;
9.1.3 购买表(purchase)
CREATE TABLE purchase(order_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单号',customer_id INT NOT NULL COMMENT '客户ID',goods_id INT NOT NULL COMMENT '商品ID',nums INT NOT NULL DEFAULT 1 COMMENT '购买数量',order_time DATETIME DEFAULT NOW(),FOREIGN KEY(customer_id) REFERENCES customer(customer_id),FOREIGN KEY(goods_id) REFERENCES goods(goods_id),INDEX idx_time(order_time)
) ENGINE=InnoDB;
9.2 设计亮点解析
-
约束组合拳:
- 客户姓名NOT NULL保证必填
- 邮箱和身份证UNIQUE防止重复
- 性别用ENUM限制取值范围
- 购买数量DEFAULT 1避免零值
-
性能优化:
- 商品分类添加索引
- 订单时间建立索引方便按时间查询
-
数据完整性:
- 外键约束确保订单中的客户和商品真实存在
- 级联操作可自行添加(根据业务需求)
9.3 扩展思考
-
库存扣减:
在购买表插入记录时,需要更新商品表库存。可以通过触发器实现:DELIMITER // CREATE TRIGGER after_purchase_insert AFTER INSERT ON purchase FOR EACH ROW BEGINUPDATE goods SET stock = stock - NEW.nums WHERE goods_id = NEW.goods_id; END// DELIMITER ;
-
订单状态:
可以添加status字段表示订单状态(待支付、已发货等),配合外键关联状态字典表 -
分区策略:
对于大规模数据,可以按订单时间做分区:CREATE TABLE purchase(...) PARTITION BY RANGE (YEAR(order_time)) (PARTITION p2022 VALUES LESS THAN (2023),PARTITION p2023 VALUES LESS THAN (2024) );