引言

提出问题

前面我们学会了增删改查,但遇到一些问题:

  • 如何防止插入重复的用户名?
  • 如何保证用户年龄不能为负数?
  • 如何保证订单必须关联到存在的用户?
  • 如何保证邮箱字段不能为空?

这就需要数据库约束(Constraints)来保证数据完整性!


数据完整性的三个层次

1. 实体完整性(Entity Integrity)

定义:每一行数据必须是唯一可识别的

实现方式:主键约束(PRIMARY KEY)

CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,  -- 主键,唯一标识
  username VARCHAR(50)
);

2. 域完整性(Domain Integrity)

定义:列的值必须符合定义的类型和约束

实现方式

  • 数据类型(INT、VARCHAR等)
  • NOT NULL约束
  • DEFAULT默认值
  • CHECK约束(MySQL 8.0.16+)
CREATE TABLE users (
  id INT PRIMARY KEY,
  age INT NOT NULL CHECK (age >= 0 AND age <= 150),  -- 年龄范围
  status TINYINT DEFAULT 1  -- 默认值
);

3. 参照完整性(Referential Integrity)

定义:表之间的关联必须一致

实现方式:外键约束(FOREIGN KEY)

CREATE TABLE orders (
  id INT PRIMARY KEY,
  user_id INT,
  FOREIGN KEY (user_id) REFERENCES users(id)  -- 外键,关联users表
);

主键约束(PRIMARY KEY)

特点

  • ✅ 唯一性:值不能重复
  • ✅ 非空性:值不能为NULL
  • ✅ 每张表只能有一个主键
  • ✅ 主键可以是单列或多列(复合主键)

单列主键

-- 方式1:列级定义
CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50)
);

-- 方式2:表级定义
CREATE TABLE users (
  id INT AUTO_INCREMENT,
  username VARCHAR(50),
  PRIMARY KEY (id)
);

复合主键

-- 学生选课表(学生ID + 课程ID作为主键)
CREATE TABLE student_courses (
  student_id INT,
  course_id INT,
  score INT,
  PRIMARY KEY (student_id, course_id)  -- 复合主键
);

-- 插入数据
INSERT INTO student_courses VALUES (1, 101, 90);  -- 成功
INSERT INTO student_courses VALUES (1, 102, 85);  -- 成功
INSERT INTO student_courses VALUES (1, 101, 95);  -- 失败:主键重复

添加/删除主键

-- 添加主键
ALTER TABLE users ADD PRIMARY KEY (id);

-- 删除主键
ALTER TABLE users DROP PRIMARY KEY;

-- 修改主键(先删除再添加)
ALTER TABLE users DROP PRIMARY KEY;
ALTER TABLE users ADD PRIMARY KEY (new_id);

唯一约束(UNIQUE)

特点

  • ✅ 值不能重复
  • ✅ 可以为NULL(但只能有一个NULL)
  • ✅ 一张表可以有多个唯一约束

基础用法

CREATE TABLE users (
  id INT PRIMARY KEY,
  username VARCHAR(50) UNIQUE,  -- 用户名唯一
  email VARCHAR(100) UNIQUE,    -- 邮箱唯一
  phone VARCHAR(20)
);

-- 插入数据
INSERT INTO users VALUES (1, 'zhangsan', 'zhang@example.com', '13800138000');
INSERT INTO users VALUES (2, 'zhangsan', 'li@example.com', '13900139000');
-- ERROR 1062: Duplicate entry 'zhangsan' for key 'username'

复合唯一约束

-- IP访问日志(同一IP在同一天只能记录一次)
CREATE TABLE access_logs (
  id INT PRIMARY KEY AUTO_INCREMENT,
  ip VARCHAR(15),
  access_date DATE,
  UNIQUE KEY uk_ip_date (ip, access_date)  -- 复合唯一约束
);

添加/删除唯一约束

-- 添加唯一约束
ALTER TABLE users ADD UNIQUE (username);
ALTER TABLE users ADD UNIQUE KEY uk_email (email);  -- 指定约束名

-- 删除唯一约束
ALTER TABLE users DROP INDEX username;
ALTER TABLE users DROP INDEX uk_email;

非空约束(NOT NULL)

特点

  • ✅ 字段不能为NULL
  • ✅ 必须提供值

基础用法

CREATE TABLE users (
  id INT PRIMARY KEY,
  username VARCHAR(50) NOT NULL,  -- 不能为空
  email VARCHAR(100) NOT NULL,
  nickname VARCHAR(50)  -- 可以为空
);

-- 插入数据
INSERT INTO users (id, username, email) VALUES (1, 'zhangsan', 'zhang@example.com');  -- 成功
INSERT INTO users (id, username) VALUES (2, 'lisi');  -- 失败:email不能为空

NULL vs 空字符串

-- NULL:未知、不存在
INSERT INTO users (id, username, email, nickname) VALUES (1, 'zhang', 'zhang@example.com', NULL);

-- 空字符串:已知但为空
INSERT INTO users (id, username, email, nickname) VALUES (2, 'li', 'li@example.com', '');

-- 查询
SELECT * FROM users WHERE nickname IS NULL;  -- 查到id=1
SELECT * FROM users WHERE nickname = '';  -- 查到id=2

重要区别

  • NULL:未知,需要用IS NULL判断
  • '':空字符串,用= ''判断

添加/删除非空约束

-- 添加非空约束
ALTER TABLE users MODIFY COLUMN phone VARCHAR(20) NOT NULL;

-- 删除非空约束
ALTER TABLE users MODIFY COLUMN phone VARCHAR(20);

默认值约束(DEFAULT)

基础用法

CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50) NOT NULL,
  status TINYINT DEFAULT 1,  -- 默认值1
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP  -- 默认当前时间
);

-- 插入数据(不指定status和created_at)
INSERT INTO users (username) VALUES ('zhangsan');

-- 查询结果
SELECT * FROM users;
-- id=1, username=zhangsan, status=1, created_at=2024-01-15 10:30:00

常用默认值

CREATE TABLE products (
  id INT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  price DECIMAL(10, 2) DEFAULT 0.00,  -- 默认价格0
  stock INT DEFAULT 0,  -- 默认库存0
  status TINYINT DEFAULT 1,  -- 默认状态1
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,  -- 创建时间
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP  -- 自动更新
);

检查约束(CHECK,MySQL 8.0.16+)

基础用法

CREATE TABLE users (
  id INT PRIMARY KEY,
  username VARCHAR(50),
  age INT CHECK (age >= 0 AND age <= 150),  -- 年龄范围
  email VARCHAR(100) CHECK (email LIKE '%@%'),  -- 邮箱格式
  status TINYINT CHECK (status IN (0, 1, 2))  -- 状态枚举
);

-- 插入数据
INSERT INTO users VALUES (1, 'zhangsan', 25, 'zhang@example.com', 1);  -- 成功
INSERT INTO users VALUES (2, 'lisi', 200, 'li@example.com', 1);  -- 失败:年龄超出范围
INSERT INTO users VALUES (3, 'wangwu', 30, 'invalid', 1);  -- 失败:邮箱格式错误

表级CHECK约束

CREATE TABLE orders (
  id INT PRIMARY KEY,
  amount DECIMAL(10, 2),
  discount DECIMAL(5, 2),
  final_amount DECIMAL(10, 2),
  CHECK (final_amount = amount - discount)  -- 金额一致性检查
);

注意事项

  • MySQL 8.0.16之前的版本不支持CHECK约束
  • CHECK约束在插入和更新时验证
  • 复杂逻辑建议在应用层验证

外键约束(FOREIGN KEY)

什么是外键?

外键:一个表中的列,引用另一个表的主键,建立表之间的关联。

基础示例

-- 父表(被引用表)
CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50) NOT NULL
);

-- 子表(引用表)
CREATE TABLE orders (
  id INT PRIMARY KEY AUTO_INCREMENT,
  user_id INT,
  order_no VARCHAR(32),
  FOREIGN KEY (user_id) REFERENCES users(id)  -- 外键约束
);

-- 插入数据
INSERT INTO users VALUES (1, 'zhangsan');
INSERT INTO orders VALUES (1, 1, 'ORDER001');  -- 成功:user_id=1存在
INSERT INTO orders VALUES (2, 999, 'ORDER002');  -- 失败:user_id=999不存在

外键的作用

  1. 保证参照完整性:订单的user_id必须存在于users表
  2. 防止删除被引用的数据:无法删除有订单的用户
-- 尝试删除有订单的用户
DELETE FROM users WHERE id = 1;
-- ERROR 1451: Cannot delete or update a parent row: a foreign key constraint fails

级联操作

CASCADE(级联)

CREATE TABLE orders (
  id INT PRIMARY KEY,
  user_id INT,
  FOREIGN KEY (user_id) REFERENCES users(id)
    ON DELETE CASCADE  -- 删除用户时,自动删除其所有订单
    ON UPDATE CASCADE  -- 更新用户ID时,自动更新订单的user_id
);

-- 删除用户
DELETE FROM users WHERE id = 1;
-- 用户被删除,其所有订单也被自动删除

SET NULL

CREATE TABLE orders (
  id INT PRIMARY KEY,
  user_id INT,
  FOREIGN KEY (user_id) REFERENCES users(id)
    ON DELETE SET NULL  -- 删除用户时,订单的user_id设为NULL
    ON UPDATE SET NULL
);

RESTRICT(默认,拒绝)

CREATE TABLE orders (
  id INT PRIMARY KEY,
  user_id INT,
  FOREIGN KEY (user_id) REFERENCES users(id)
    ON DELETE RESTRICT  -- 有订单的用户无法删除
    ON UPDATE RESTRICT
);

NO ACTION(与RESTRICT类似)

CREATE TABLE orders (
  id INT PRIMARY KEY,
  user_id INT,
  FOREIGN KEY (user_id) REFERENCES users(id)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION
);

外键的优缺点

优点

  • ✅ 保证数据一致性
  • ✅ 数据库层面约束,更可靠
  • ✅ 自动级联操作

缺点

  • ❌ 性能开销(每次插入/更新都要检查)
  • ❌ 影响分库分表(外键跨库无法使用)
  • ❌ 影响高并发(增加锁竞争)

生产环境建议

互联网公司通常不使用外键

  • 数据一致性在应用层保证
  • 提升性能和扩展性
  • 便于分库分表
-- 推荐做法:不加外键约束,通过应用层保证一致性
CREATE TABLE orders (
  id INT PRIMARY KEY,
  user_id INT,  -- 不加FOREIGN KEY
  INDEX idx_user_id (user_id)  -- 只加索引,提升查询性能
);

实战案例:电商订单系统

表设计

-- 用户表
CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50) NOT NULL UNIQUE,
  email VARCHAR(100) NOT NULL UNIQUE,
  balance DECIMAL(10, 2) DEFAULT 0.00 CHECK (balance >= 0),
  status TINYINT DEFAULT 1 CHECK (status IN (0, 1)),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 商品表
CREATE TABLE products (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(200) NOT NULL,
  price DECIMAL(10, 2) NOT NULL CHECK (price > 0),
  stock INT DEFAULT 0 CHECK (stock >= 0),
  status TINYINT DEFAULT 1,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 订单表
CREATE TABLE orders (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id INT NOT NULL,
  order_no VARCHAR(32) NOT NULL UNIQUE,
  total_amount DECIMAL(10, 2) NOT NULL CHECK (total_amount > 0),
  status TINYINT DEFAULT 0 CHECK (status IN (0, 1, 2, 3, 4)),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_user_id (user_id),
  INDEX idx_order_no (order_no)
);

-- 订单详情表
CREATE TABLE order_items (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_id BIGINT NOT NULL,
  product_id INT NOT NULL,
  quantity INT NOT NULL CHECK (quantity > 0),
  price DECIMAL(10, 2) NOT NULL CHECK (price > 0),
  subtotal DECIMAL(10, 2) NOT NULL,
  INDEX idx_order_id (order_id),
  INDEX idx_product_id (product_id)
);

约束总结

约束类型作用
usersPRIMARY KEYid唯一
usersUNIQUEusername、email不重复
usersNOT NULL必填字段
usersCHECKbalance>=0, status枚举
usersDEFAULT默认值
orders索引代替外键,提升性能

最佳实践

约束命名规范

-- 主键:pk_表名
ALTER TABLE users ADD CONSTRAINT pk_users PRIMARY KEY (id);

-- 唯一约束:uk_表名_列名
ALTER TABLE users ADD CONSTRAINT uk_users_username UNIQUE (username);

-- 外键:fk_表名_引用表名
ALTER TABLE orders ADD CONSTRAINT fk_orders_users
  FOREIGN KEY (user_id) REFERENCES users(id);

-- 检查约束:ck_表名_列名
ALTER TABLE users ADD CONSTRAINT ck_users_age
  CHECK (age >= 0 AND age <= 150);

选择建议

场景推荐约束
唯一标识PRIMARY KEY
不允许重复UNIQUE
不允许为空NOT NULL
数值范围CHECK
表关联(小项目)FOREIGN KEY
表关联(大项目)索引 + 应用层保证

总结

核心要点

  1. 主键约束:唯一 + 非空,每表一个
  2. 唯一约束:不能重复,可为NULL
  3. 非空约束:不能为NULL
  4. 默认值:未提供值时使用默认值
  5. 检查约束:限制值的范围(MySQL 8.0.16+)
  6. 外键约束:保证参照完整性(大项目不推荐)

记忆口诀

约束六兄弟:主唯非默查外

  • :PRIMARY KEY(主键)
  • :UNIQUE(唯一)
  • :NOT NULL(非空)
  • :DEFAULT(默认值)
  • :CHECK(检查)
  • :FOREIGN KEY(外键)

下一步学习

  • 上一篇:《基础增删改查:DML操作入门》
  • 下一篇:《字符集与校对规则》
  • 返回目录:MySQL从入门到精通

💡 本文是 “MySQL从入门到精通” 系列的第 7 篇(共86篇)。

📚 建议动手练习各种约束,理解其作用和限制。


参考资料

  1. MySQL官方文档 - 约束
  2. MySQL官方文档 - 外键