05.MySQL表的约束

MySQL表的约束

MySQL表的约束
[1. 空属性](#1. 空属性)
[2. 默认值](#2. 默认值)
[3. 列描述](#3. 列描述)
[4. zerofill](#4. zerofill)
[5. 主键](#5. 主键)
[6. 自增长](#6. 自增长)
[7. 唯一键](#7. 唯一键)
[8. 外键](#8. 外键)
[9. 综合案例](#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。比如班级表的教室字段:

sql 复制代码
CREATE TABLE class(
    class_name VARCHAR(20) NOT NULL,
    classroom VARCHAR(20) NOT NULL
);

这时候插入数据时,这两个字段就必须填,否则MySQL会直接报错:

sql 复制代码
INSERT INTO class(class_name) VALUES('三年二班');
-- 报错:Field 'classroom' doesn't have a default value

1.2 NULL的隐藏陷阱

很多人以为NULL就是"没有值",其实它更像"未知值"。比如两个NULL比较时,既不等于也不等于不,永远返回UNKNOWN。这会导致查询时出现意想不到的结果:

sql 复制代码
SELECT * FROM users WHERE email = NULL; -- 查不到任何数据
SELECT * FROM users WHERE email IS NULL; -- 才能查到空邮箱用户

1.3 NOT NULL的进阶玩法

有时候我们会给字段设置默认值,这时候再加NOT NULL就显得多余了。比如:

sql 复制代码
CREATE TABLE user(
    id INT PRIMARY KEY AUTO_INCREMENT,
    gender ENUM('男','女') DEFAULT '男' NOT NULL
);

这里的NOT NULL其实可以省略,因为默认值已经保证了字段不为空。但要注意默认值的类型匹配,比如用字符串'0'当默认值,字段类型却是INT的话,可能会触发隐式转换。


2. 默认值

2.1 DEFAULT的魔法时刻

默认值就像自动售货机------当你不投币时,它自己吐出预设的商品。比如用户表的性别字段:

sql 复制代码
CREATE TABLE user(
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    gender ENUM('男','女') DEFAULT '男'
);

这时候插入数据时,如果不指定gender字段,就会自动填充"男":

sql 复制代码
INSERT INTO user(name) VALUES('张三');
-- 实际插入的是('张三', '男')

2.2 默认值的进阶套路

默认值不仅能用常量,还能用表达式(MySQL 8.0+支持):

sql 复制代码
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会自动设置当前时间作为默认值(如果未显式指定)

比如:

sql 复制代码
CREATE TABLE test(
    id INT PRIMARY KEY AUTO_INCREMENT,
    dt DATETIME,           -- 默认值为NULL
    ts TIMESTAMP           -- 默认值自动变为CURRENT_TIMESTAMP
);

3. 列描述

3.1 COMMENT的文艺复兴

列描述就像给数据库字段写小纸条,方便后来人看懂你的设计思路。比如用户表:

sql 复制代码
CREATE TABLE user(
    id INT PRIMARY KEY COMMENT '用户ID',
    name VARCHAR(20) COMMENT '用户真实姓名',
    nickname VARCHAR(20) COMMENT '用户昵称(可为空)'
);

通过SHOW CREATE TABLE user;就能看到这些注释:

text 复制代码
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 描述的最佳实践

  1. 业务含义 :说明字段的业务逻辑,比如status TINYINT COMMENT '状态:1-待支付 2-已支付 3-已发货'
  2. 数据来源 :标明数据是怎么来的,比如score DECIMAL(5,2) COMMENT '根据考试成绩自动同步'
  3. 变更记录 :重要字段的变更历史,比如address VARCHAR(255) COMMENT '2022.3新增,原地址字段已废弃'

3.3 描述的隐藏功能

结合INFORMATION_SCHEMA.COLUMNS表,可以实现自动化文档生成:

sql 复制代码
SELECT COLUMN_NAME, COLUMN_COMMENT 
FROM information_schema.columns
WHERE TABLE_SCHEMA='your_db' AND TABLE_NAME='user';

4. zerofill

4.1 数字显示的艺术

zerofill就像给数字穿上西装打上领带------让它看起来更正式。比如订单编号:

sql 复制代码
CREATE TABLE orders(
    order_id INT(6) ZEROFILL PRIMARY KEY AUTO_INCREMENT
);

插入数据时:

sql 复制代码
INSERT INTO orders() VALUES();
-- 实际显示order_id为000001

注意这里的INT(6)不是指6位数字,而是显示宽度。底层存储还是标准的INT类型(4字节)。

4.2 zerofill的连带效应

使用zerofill会自动触发UNSIGNED属性:

sql 复制代码
CREATE TABLE test(a INT(5) ZEROFILL);
-- 实际相当于 INT(5) UNSIGNED ZEROFILL

所以字段只能存储正数,负数插入会变成0。

4.3 使用场景分析

适合需要固定显示位数的业务场景:

  • 学号:202301010001(年份+学院代码+序号)
  • 发票号码:0000123456
  • 产品编号:P000001

但要注意,这种格式化更适合前端处理,数据库层面保持纯粹数字更利于计算。


5. 主键

5.1 数据的身份证

主键就像每个人的身份证号码------必须唯一且不能为空。创建学生表:

sql 复制代码
CREATE TABLE student(
    stu_id CHAR(10) PRIMARY KEY COMMENT '学号(唯一标识)',
    name VARCHAR(20)
);

这时候插入重复学号会直接报错:

sql 复制代码
INSERT INTO student VALUES('2023010101','张三');
INSERT INTO student VALUES('2023010101','李四');
-- 报错:Duplicate entry '2023010101' for key 'PRIMARY'

5.2 主键的进阶操作

  1. 删除主键

    sql 复制代码
    ALTER TABLE student DROP PRIMARY KEY;

    注意:如果该列有自增属性,需要先删除自增

  2. 修改主键

    sql 复制代码
    ALTER TABLE student MODIFY stu_id CHAR(12);
    -- 修改字段类型不影响主键约束
  3. 复合主键

    sql 复制代码
    CREATE TABLE cart(
        user_id INT,
        product_id INT,
        quantity INT,
        PRIMARY KEY(user_id, product_id)
    );

    这时候单个字段可以重复,但组合必须唯一

5.3 主键选择的玄学

  1. 自增主键 vs 业务主键

    自增主键(如AUTO_INCREMENT)更利于索引性能,业务主键(如身份证号)更直观,需要根据场景权衡

  2. UUID的另类玩法

    sql 复制代码
    CREATE TABLE orders(
        order_id CHAR(36) PRIMARY KEY DEFAULT UUID(),
        ...
    );

    适合分布式系统,但会占用更多存储空间


6. 自增长

6.1 AUTO_INCREMENT的魔法

自增字段就像自动步枪------每次扣动扳机都会自动装弹。创建文章表:

sql 复制代码
CREATE TABLE articles(
    article_id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(100)
);

插入数据时:

sql 复制代码
INSERT INTO articles(title) VALUES('MySQL约束详解');
-- article_id自动分配为1
INSERT INTO articles(title) VALUES('性能优化技巧');
-- article_id自动分配为2

6.2 自增的隐藏技巧

  1. 指定初始值

    sql 复制代码
    CREATE TABLE users(
        id INT AUTO_INCREMENT PRIMARY KEY
    ) AUTO_INCREMENT = 1000;
  2. 跳增现象

    如果插入失败或事务回滚,自增值不会回退。比如插入100后失败,下一个值会是101而不是100

  3. 手动赋值

    sql 复制代码
    INSERT INTO articles(article_id, title) VALUES(10000, '特别推荐');
    -- 下次自增值从10001开始

6.3 自增的性能考量

  1. 并发问题

    InnoDB引擎使用互斥锁来确保自增的连续性,在高并发场景可能成为瓶颈

  2. 缓存配置
    innodb_autoinc_lock_mode参数影响自增行为,需要根据业务调整

  3. 迁移风险

    导出导入数据时,记得检查自增字段的当前值:

    sql 复制代码
    SHOW TABLE STATUS LIKE 'articles';
    -- 查看Auto_increment列

7. 唯一键

7.1 除主键外的唯一保障

唯一键就像班级里的学号和电话号码------都可以唯一标识学生,但只能选一个当主键。创建用户表:

sql 复制代码
CREATE TABLE user(
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(20) UNIQUE,
    email VARCHAR(50) UNIQUE
);

这时候用户名和邮箱都必须唯一:

sql 复制代码
INSERT INTO user(username, email) 
VALUES('zhangsan', '[email protected]');

-- 插入相同用户名会失败
INSERT INTO user(username, email) 
VALUES('zhangsan', '[email protected]');
-- 报错:Duplicate entry 'zhangsan' for key 'username'

7.2 唯一键的骚操作

  1. 复合唯一键

    sql 复制代码
    CREATE TABLE exam(
        student_id INT,
        subject VARCHAR(20),
        score INT,
        UNIQUE(student_id, subject)
    );

    保证同一个学生同一科目只有一条记录

  2. 空值处理

    唯一键允许有多个NULL值(这与主键不同):

    sql 复制代码
    INSERT INTO user(username) VALUES(NULL);
    INSERT INTO user(username) VALUES(NULL);
    -- 两条记录都能成功插入
  3. 延迟约束

    在事务中,唯一约束检查可以延迟到提交时:

    sql 复制代码
    SET CONSTRAINTS ALL DEFERRED;
    -- 需要配合支持的存储引擎

7.3 唯一键的优化技巧

  1. 前缀索引

    对长字符串字段,可以只索引前缀:

    sql 复制代码
    CREATE TABLE products(
        id INT PRIMARY KEY,
        product_code VARCHAR(100) UNIQUE
    );
    -- 改为
    CREATE UNIQUE INDEX idx_code ON products(product_code(20));
  2. 合并索引

    如果某个查询经常同时用到username和email,可以创建联合唯一索引:

    sql 复制代码
    CREATE UNIQUE INDEX idx_user_email ON user(username, email);

8. 外键

8.1 表之间的羁绊

外键就像亲子关系------孩子必须知道自己爹是谁。创建订单表:

sql 复制代码
CREATE TABLE orders(
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    FOREIGN KEY(user_id) REFERENCES users(id)
);

这时候插入订单时:

sql 复制代码
-- 假设users表中没有id=100的用户
INSERT INTO orders(user_id) VALUES(100);
-- 报错:Cannot add or update a child row

8.2 外键的连环反应

  1. 级联更新

    当父表主键变更时,子表自动更新:

    sql 复制代码
    CREATE TABLE orders(
        order_id INT PRIMARY KEY AUTO_INCREMENT,
        user_id INT,
        FOREIGN KEY(user_id) REFERENCES users(id) ON UPDATE CASCADE
    );
  2. 级联删除

    删除父表记录时,自动删除子表关联数据:

    sql 复制代码
    FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
  3. 置空操作

    删除父表记录时,将子表外键字段设为NULL:

    sql 复制代码
    FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL
    -- 注意:字段必须允许NULL

8.3 外键的性能博弈

  1. 锁机制

    修改父表主键时,会锁定子表相关记录,可能引发死锁

  2. 批量导入

    导入大量数据时,建议先关闭外键检查:

    sql 复制代码
    SET FOREIGN_KEY_CHECKS=0;
    -- 执行导入操作
    SET FOREIGN_KEY_CHECKS=1;
  3. 索引优化

    外键字段必须有索引,否则会影响性能:

    sql 复制代码
    CREATE INDEX idx_user ON orders(user_id);
    -- 如果创建外键时未自动创建

9. 综合案例

9.1 商城系统的数据设计

让我们来设计一个简单的电商系统,包含三个核心表:

9.1.1 商品表(goods)
sql 复制代码
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)
sql 复制代码
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)
sql 复制代码
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 设计亮点解析

  1. 约束组合拳

    • 客户姓名NOT NULL保证必填
    • 邮箱和身份证UNIQUE防止重复
    • 性别用ENUM限制取值范围
    • 购买数量DEFAULT 1避免零值
  2. 性能优化

    • 商品分类添加索引
    • 订单时间建立索引方便按时间查询
  3. 数据完整性

    • 外键约束确保订单中的客户和商品真实存在
    • 级联操作可自行添加(根据业务需求)

9.3 扩展思考

  1. 库存扣减

    在购买表插入记录时,需要更新商品表库存。可以通过触发器实现:

    sql 复制代码
    DELIMITER //
    CREATE TRIGGER after_purchase_insert 
    AFTER INSERT ON purchase
    FOR EACH ROW
    BEGIN
        UPDATE goods SET stock = stock - NEW.nums 
        WHERE goods_id = NEW.goods_id;
    END//
    DELIMITER ;
  2. 订单状态

    可以添加status字段表示订单状态(待支付、已发货等),配合外键关联状态字典表

  3. 分区策略

    对于大规模数据,可以按订单时间做分区:

    sql 复制代码
    CREATE TABLE purchase(...) 
    PARTITION BY RANGE (YEAR(order_time)) (
        PARTITION p2022 VALUES LESS THAN (2023),
        PARTITION p2023 VALUES LESS THAN (2024)
    );
相关推荐
博睿谷IT99_5 分钟前
Oracle OCP与MySQL OCP认证如何选?
mysql·oracle·开闭原则
小艺E6 分钟前
联通专线赋能,亿林网络裸金属服务器:中小企业 IT 架构升级优选方案
服务器·网络·架构
deeper_wind8 分钟前
防火墙设置实战操作案例(小白的“升级打怪”成长之路)
linux·运维·网络
我想睡觉26110 分钟前
Python训练营打卡DAY44
开发语言·人工智能·python·深度学习·算法·机器学习
czy878747517 分钟前
正点原子lwIP协议的学习笔记
笔记·网络协议·学习·tcp/ip
crack_comet21 分钟前
Windows下运行Redis并设置为开机自启的服务
数据库·windows·redis
理***所21 分钟前
湖北理元理律师事务所:法律视角下的债务优化与生活平衡之道
数据库
suzhou_speeder28 分钟前
网络交换机:构建高效、安全、灵活局域网的基石
网络·安全·交换机·poe·poe交换机
黄雪超30 分钟前
JVM——JVM中的字节码:解码Java跨平台的核心引擎
java·开发语言·jvm
hiii30 分钟前
【ART 内存管理】堆内存分布与分配
android